TuneCamp Internationalization (i18n) β Agile Plan β
Goal: Ship TuneCamp with full UI support for at least two languages β English (en) and Italian (it) β with an architecture that lets us add more locales later without touching component code.
Status: Proposed Owner: TBDTarget: 3 sprints (~6 weeks, 1 dev part-time)
1. Context & Current State β
- The webapp is React 19 + Vite + TypeScript, with Zustand stores, TanStack Query and React Router (
webapp/src, ~113.tsxfiles, 42 pages, 69 components). - No i18n framework is installed today. All UI copy is hardcoded inline in JSX.
- Copy is currently a mix of English and Italian (e.g. the admin Setup Wizard was Italian; now translated to English). This inconsistency is exactly what this plan fixes structurally.
- Docs already ship an Italian mirror under
docs/it/, so bilingual intent exists β we now bring it to the product UI.
Design principle: English is the source/default locale (fallback). Italian is the first translated target. Every string lives in a resource file keyed by a stable ID, never inline.
2. Recommended Stack β
| Concern | Choice | Why |
|---|---|---|
| Framework | react-i18next + i18next | De-facto standard for React, hooks-based (useTranslation), plays well with Vite, supports interpolation/plurals/namespaces, lazy-loading. |
| Language detection | i18next-browser-languagedetector | Reads localStorage β browser navigator.language, with a sane fallback chain. |
| Format (dates/numbers/currency) | Intl (built-in) via i18next Intl formatters | No extra dependency; currency matters for the Store. |
| Storage of user choice | localStorage key tc_lang + persisted per-user via existing settings API (optional, Sprint 3) | Anonymous users get a local preference; logged-in users can persist. |
Rejected alternatives:
react-intl(heavier, more boilerplate),lingui(great but adds a compile step + macro tooling we don't need yet).
Resource file layout:
webapp/src/i18n/
index.ts # i18next init + config
locales/
en/
common.json # buttons, generic labels, nav
admin.json # admin panels + setup wizard
auth.json # login / register / password
player.json # player, now-playing, queue
store.json # store, checkout, payments
errors.json # error + toast messages
it/
common.json
admin.json
...Namespaces keep bundles small and let us lazy-load per route.
3. Epics β
EPIC A β i18n Foundation β
Install and wire the framework; app renders through the translation layer with English as the only (fully-covered) locale. No visible change yet.
EPIC B β String Extraction & English Catalog β
Replace every hardcoded UI string with a t('key') call and populate the en catalog. This is the bulk of the work and is done incrementally, namespace by namespace, so it never blocks a release.
EPIC C β Italian Translation β
Produce the it catalog for every extracted key. Native/fluent review pass. Currency, dates, and pluralization verified.
EPIC D β Language Switcher & Persistence β
User-facing language selector (header/settings), detection, persistence, <html lang> sync, and (optional) per-user server-side preference.
EPIC E β QA, Guardrails & Docs β
Lint rule to block new hardcoded strings, a "missing key" report, pseudo-localization test, and contributor docs so the two-language guarantee doesn't rot.
4. Sprint Breakdown β
Sprint 1 β Foundation + First Vertical Slice β
Objective: Framework live in production behind English; prove the pattern end-to-end on 2β3 high-value surfaces.
- [ ] A1 Add deps (
i18next,react-i18next,i18next-browser-languagedetector); createsrc/i18n/index.ts; wrap<App>provider inmain.tsx. (2 pts) - [ ] A2 Define namespace list +
en/itempty scaffolding; TS types for keys (i18nexttype augmentation for autocomplete + compile-time safety). (2 pts) - [ ] B1 Extract
common.json(nav, buttons: Save/Cancel/Next/Back, generic labels) β highest reuse. (3 pts) - [ ] B2 Extract
admin.jsonstarting with the Setup Wizard (already English, good pilot) +auth.json. (5 pts) - [ ] C0 Provide
itvalues for everything extracted in this sprint (wizard, auth, common). (3 pts) - [ ] D1 Minimal language switcher (EN/IT toggle) in the header, wired to
i18n.changeLanguage+localStorage. (2 pts)
Sprint 1 acceptance: Toggling ENβIT instantly re-renders the header, nav, auth screens and the Setup Wizard in the chosen language; refresh preserves the choice; English remains fallback for anything not yet translated.
Sprint 2 β Broad Extraction β
Objective: Convert the remaining high-traffic surfaces.
- [ ] B3
player.jsonβ player, queue, now-playing. (3 pts) - [ ] B4
store.jsonβ store, release/album pages, checkout, payment states; wireIntl.NumberFormatcurrency formatting. (5 pts) - [ ] B5
errors.jsonβ toasts, form validation, API error surfacing. (3 pts) - [ ] B6 Remaining pages/components sweep (social, playlists, profiles, admin panels). (8 pts, may spill)
- [ ] C1 Italian catalog for all Sprint-2 keys + fluency review pass. (5 pts)
- [ ] D2 Date/relative-time via
Intllocale-aware formatters (replace any hardcoded formatting). (2 pts)
Sprint 2 acceptance: β₯90% of user-visible strings route through t(); store prices and dates render correctly in both locales; no English leaks on the main listener + buyer flows when set to Italian.
Sprint 3 β Hardening, Persistence & Guardrails β
Objective: Make it durable and contributor-safe.
- [ ] D3 Persist language per logged-in user via settings API; hydrate on login; sync
document.documentElement.lang. (3 pts) - [ ] E1 ESLint rule (
i18next/no-literal-stringor equivalent) inwebapp/eslint.config.js, scoped to JSX, to block new hardcoded copy. (2 pts) - [ ] E2 CI check / script that reports missing or orphaned keys across locales (fail build on missing
en; warn on missingit). (3 pts) - [ ] E3 Pseudo-locale (
en-XX) dev toggle to catch untranslated/overflowing strings visually. (2 pts) - [ ] B7 Final sweep of long tail (admin edge panels, empty states, tooltips,
aria-labels,<title>/meta). (5 pts) - [ ] C2 Italian final QA pass with a native speaker (scobru); fix tone/terminology. (3 pts)
- [ ] E4 Contributor docs:
docs/i18n.md(how to add a string, how to add a locale) + note indocs/development-guide.md. (2 pts)
Sprint 3 acceptance: New PRs cannot introduce raw JSX strings without a lint failure; a locale can be added by dropping a folder under locales/ and registering it; both en and it are 100% covered; language choice survives login/logout across devices for logged-in users.
5. Backlog / Later (out of scope for the 2-language MVP) β
- Additional locales (ES, FR, DE) β trivial once the pipeline exists; each is "translate the catalog."
- Server-rendered / federation-facing strings and email/notification templates.
- Localizing user-generated content (release descriptions, bios) β explicitly not in scope; we localize the chrome, not the catalog content.
- Translation-management integration (Crowdin/Weblate) if community translators join.
- RTL layout support (only relevant when an RTL locale is added).
6. Key Decisions & Conventions β
- Source locale =
en. All keys authored in English first;enis always the fallback. - No inline strings. Every user-visible string uses
t('namespace:key'). Enforced by lint (Sprint 3). - Stable, semantic keys (
wizard.step.profile.title), not English-text-as-key β so copy edits don't churn keys. - Namespaces map to feature areas and lazy-load per route to keep bundles small.
- Interpolation over concatenation for variables/plurals (
t('store.items', { count })), never string-building in JSX. - Definition of Done for any UI PR going forward: new strings exist in
enandit(missingit= warning, missingen= build failure).
7. Risks & Mitigations β
| Risk | Mitigation |
|---|---|
| Extraction is large (113 files) and can stall mid-way | Namespace-by-namespace, shippable each step; English fallback means partial extraction never breaks the UI. |
| New PRs reintroduce hardcoded strings | ESLint guardrail (E1) + DoD checklist. |
| Italian drifts out of sync with English | CI missing-key report (E2) + fallback to English so gaps degrade gracefully. |
| Currency/number/date bugs in the Store | Centralize on Intl formatters; explicit test cases for EUR/USD and IT date format. |
| Bundle-size regression | Namespaced lazy loading; measure with vite build report. |
8. First Concrete Step β
Sprint 1 / A1: add the dependencies and the src/i18n/index.ts init, wrap the app, and migrate the Setup Wizard (just made English) as the pilot namespace β it's self-contained and gives us the full EN + IT round-trip to validate the whole approach before the broad sweep.