# PRD — MSP i18n Infrastructure Sprint (Phase 0) - Slug: `msp-i18n-infrastructure-sprint` - Date: `2026-02-19` - Status: Draft - Parent plan: `docs/plans/2026-02-18-msp-i18n-full-translation-plan.md` - Depends on: `docs/plans/2026-02-12-msp-i18n-phase1/` (completed) ## Summary Prepare the i18n infrastructure for batch translation of the entire MSP portal. This sprint makes five changes: (a) lazy namespace loading so only route-relevant namespaces are fetched, (b) pseudo-locale generation for visual QA, (c) rename `msp.json` to `msp/core.json`, (d) update I18nWrapper to load namespaces by route, and (e) sync the core config package. When complete, any subsequent translation batch can be added as a new namespace file without architectural changes. ## Problem Phase 1 established the MSP i18n foundation (feature flag, I18nWrapper, namespace restructuring, language settings). But the current config eagerly loads ALL namespaces on every page (`ns: ['common', 'client-portal', 'msp']`). As 12+ MSP feature namespaces are added, this becomes 20+ HTTP requests per page load. Additionally, there's no way to visually verify that all hardcoded strings have been extracted. The `msp.json` filename doesn't match the planned `msp/core.json` convention. ## Goals - **G1**: Change i18next to only load `common` on init, with additional namespaces loaded on-demand per route - **G2**: Add `ROUTE_NAMESPACES` mapping and `getNamespacesForRoute()` helper to config - **G3**: Update `I18nWrapper` to use `usePathname()` and load route-appropriate namespaces - **G4**: Create `scripts/generate-pseudo-locale.ts` that generates `xx` and `yy` pseudo-locale files - **G5**: Add pseudo-locales (`xx`, `yy`) to config - **G6**: Rename `msp.json` to `msp/core.json` for all 7 languages - **G7**: Update all references from `useTranslation('msp')` to `useTranslation('msp/core')` - **G8**: Keep `packages/core/src/lib/i18n/config.ts` in sync with `packages/ui/src/lib/i18n/config.ts` - **G9**: Client portal continues working with zero regressions - **G10**: MSP portal with flag OFF continues working with zero regressions ## Non-goals - Translating any new MSP page content (that's Batch 1+) - Creating the actual `msp/settings.json` or other feature namespace files - Server-side namespace preloading or caching - CI/CD translation validation pipeline - Changing the feature flag behavior ## Users and Primary Flows ### Persona: Developer adding a translation batch **Flow 1: Adding a new MSP feature namespace** 1. Developer creates `server/public/locales/en/msp/.json` 2. Developer adds route entry to `ROUTE_NAMESPACES` in config 3. Namespaces auto-load when users navigate to that route 4. No changes to I18nWrapper, I18nProvider, or layout files needed **Flow 2: Visual QA with pseudo-locales** 1. Developer runs `npx ts-node scripts/generate-pseudo-locale.ts --locale xx --fill 1111` 2. Pseudo-locale files generated for all existing namespaces 3. Developer enables `msp-i18n-enabled` flag locally 4. Developer switches browser to `xx` locale 5. All translated strings show `1111`; untranslated strings remain in English (= missed extraction) ### Persona: End user (MSP portal) **Flow 3: Flag OFF (default) — no change** - MSP portal stays English-only, identical to today - No additional HTTP requests for namespace files **Flow 4: Flag ON — navigating MSP portal** - Only `common` + `msp/core` + route-specific namespaces are loaded - No loading of irrelevant namespaces (e.g., `msp/settings` doesn't load on `/msp/tickets`) ### Persona: End user (Client portal) **Flow 5: Client portal — no change** - Client portal continues loading `common` + `client-portal` + relevant `features/*` namespaces - Zero regressions from namespace renaming or config changes ## UX / UI Notes No user-facing UI changes in this sprint. All changes are developer-facing infrastructure. ## Requirements ### Functional Requirements #### Lazy Namespace Loading - **FR1**: `I18N_CONFIG.ns` changed from `['common', 'client-portal', 'msp']` to `['common']` - **FR2**: `ROUTE_NAMESPACES` constant exported from `packages/ui/src/lib/i18n/config.ts` mapping route prefixes to namespace arrays - **FR3**: `getNamespacesForRoute(pathname: string): string[]` function exported from config — exact match first, then longest prefix match, fallback to `['common']` - **FR4**: `I18nWrapper` reads `usePathname()` and passes resolved namespaces to `I18nProvider` - **FR5**: `I18nProvider` calls `i18next.loadNamespaces()` for any namespaces not yet loaded when route changes - **FR6**: `I18nProvider` accepts optional `namespaces` prop for route-specific namespace list #### Pseudo-Locale Generation - **FR7**: `scripts/generate-pseudo-locale.ts` script exists and is runnable with `npx ts-node` - **FR8**: Script accepts `--locale ` and `--fill ` CLI arguments - **FR9**: Script reads all English namespace JSON files (including nested `features/` and `msp/` directories) - **FR10**: Script outputs corresponding files under `server/public/locales//` preserving directory structure - **FR11**: Script replaces all leaf string values with the fill string - **FR12**: Script preserves `{{variables}}` within the fill string (e.g., `"1111 {{name}} 1111"`) - **FR13**: Script preserves JSON key structure exactly - **FR14**: Pseudo-locales `xx` and `yy` added to `LOCALE_CONFIG.supportedLocales` #### Namespace Rename (msp.json → msp/core.json) - **FR15**: `server/public/locales/{lang}/msp.json` renamed to `server/public/locales/{lang}/msp/core.json` for all 7 languages - **FR16**: All `useTranslation('msp')` references updated to `useTranslation('msp/core')` - **FR17**: `I18N_CONFIG.ns` no longer lists `'msp'` (it now only lists `'common'`) - **FR18**: `ROUTE_NAMESPACES` uses `'msp/core'` (not `'msp'`) #### Config Sync - **FR19**: `packages/core/src/lib/i18n/config.ts` updated to match any locale config changes in `packages/ui/src/lib/i18n/config.ts` #### Backward Compatibility - **FR20**: Client portal loads correct namespaces per route (no regressions) - **FR21**: MSP portal with flag OFF has zero behavior change - **FR22**: MSP portal with flag ON loads `msp/core` namespace correctly - **FR23**: Existing Phase 1 tests continue passing (with updated namespace name `msp/core`) ## Data / API / Integrations No database changes. No new API endpoints. Translation files served as static JSON from `server/public/locales/`. ### Key file changes | File | Change | |------|--------| | `packages/ui/src/lib/i18n/config.ts` | Add `ROUTE_NAMESPACES`, `getNamespacesForRoute()`, change `ns` to `['common']`, add pseudo-locales | | `packages/core/src/lib/i18n/config.ts` | Sync locale config changes | | `packages/ui/src/lib/i18n/client.tsx` | `I18nProvider` accepts `namespaces` prop, calls `loadNamespaces()` on route change | | `packages/tenancy/src/components/i18n/I18nWrapper.tsx` | Use `usePathname()` + `getNamespacesForRoute()` to pass namespaces to `I18nProvider` | | `server/public/locales/{lang}/msp.json` | Renamed to `server/public/locales/{lang}/msp/core.json` (7 languages) | | `scripts/generate-pseudo-locale.ts` | New file — pseudo-locale generator | ## Rollout / Migration 1. Rename `msp.json` → `msp/core.json` (7 files moved, old files removed) 2. Update config and wrapper in one PR 3. Update Phase 1 tests to reference `msp/core` instead of `msp` 4. All behind existing `msp-i18n-enabled` flag — zero user impact 5. Pseudo-locale files not committed to git (generated on demand, .gitignore the `xx/` and `yy/` directories) ## Open Questions - Should pseudo-locale directories be gitignored or committed? (Recommendation: gitignore — they're generated artifacts) - Should `ROUTE_NAMESPACES` include entries for routes that don't have feature namespaces yet (e.g., `/msp/settings` before `msp/settings.json` exists)? (Recommendation: yes, with just `['common', 'msp/core']` — the missing namespace will simply not be fetched until the file exists) ## Acceptance Criteria (Definition of Done) 1. `I18N_CONFIG.ns` is `['common']` — only common loaded on init 2. Navigating to `/msp/tickets` loads `common`, `msp/core`, `features/tickets` (and NOT `client-portal`, `msp/settings`, etc.) 3. Navigating to `/client-portal/billing` loads `common`, `client-portal`, `features/billing` 4. `scripts/generate-pseudo-locale.ts` generates correct pseudo files for all namespaces 5. Pseudo files preserve `{{variables}}` and JSON structure 6. `msp/core.json` exists for all 7 languages with same content as old `msp.json` 7. Old `msp.json` files are removed 8. `useTranslation('msp/core')` returns correct translations 9. Client portal has zero regressions 10. MSP portal with flag OFF has zero behavior change 11. `npm run build` succeeds 12. Phase 1 tests pass (updated to `msp/core` namespace)