# MSP Full Translation Plan - Slug: `msp-i18n-full-translation` - Date: `2026-02-18` - Status: Approved - Supersedes: `.ai/translation/MSP_i18n_plan.md` (phases 2-7) - Builds on: `docs/plans/2026-02-12-msp-i18n-phase1/` (completed) ## Summary Translate the entire MSP portal into all 7 supported languages (en, fr, es, de, nl, it, pl) through a repeatable batch process. Each batch extracts one feature area's hardcoded strings, migrates date/number formatting to locale-aware utilities, adds error/validation translation keys, and ships behind the existing `msp-i18n-enabled` feature flag. ## Current State (Post-Phase 1) Phase 1 is 100% complete (all 33 features merged). The foundation is in place: | What | Status | |------|--------| | `msp-i18n-enabled` feature flag | Done | | `I18nWrapper` in MSP layout (standard + EE) | Done | | Namespace restructuring (`clientPortal.json` split into `client-portal.json` + `features/*.json`) | Done | | Client portal components migrated to new namespaces | Done | | `msp.json` with nav/sidebar/header/settings labels | Done (73 lines, ~60 keys) | | MSP org language settings (Settings > General) | Done | | Personal language preference in Profile | Done | | Locale hierarchy (user > org default > system default) | Done | | 7 languages configured | Done | | MSP page content translated | **0%** — no MSP components call `useTranslation()` yet | | Date/number formatting locale-aware | **~5%** — `useFormatters()` hook exists but rarely used | ### Current translation file inventory ``` server/public/locales/en/ common.json 878 lines (~400 keys) client-portal.json 457 lines (~300 keys) msp.json 73 lines (~60 keys) features/ appointments.json 138 lines billing.json 98 lines documents.json 189 lines projects.json 99 lines tickets.json 175 lines ``` All 7 languages have matching files. ## Strategy ``` Infrastructure sprint | v +---> Batch N ----+ | 1. Inventory | | 2. Create EN namespace JSON | 3. Extract strings (t('key') calls) | 4. Migrate formatting (useFormatters) | 5. Add error/validation keys | 6. Generate pseudo-locale files | 7. Visual QA with pseudo-locales | 8. Generate real translations (AI) | 9. Validate (keys, JSON, variables) | 10. Ship (PR, merge) +-----+ | v (repeat for all batches) Rollout & cleanup ``` Three pillars per batch: 1. **UI string extraction** — replace hardcoded English with `t('key')` calls 2. **Date/time/number formatting** — replace hardcoded `'en-US'` formatting with `useFormatters()` hook 3. **Error/validation messages** — follow client portal pattern (server returns English, component maps to translation keys) --- ## Phase 0: Infrastructure Sprint A focused sprint that creates the foundation for all subsequent batches. Everything here happens before any MSP page content gets translated. ### 0a. Lazy Namespace Loading **Problem:** `I18N_CONFIG.ns` lists all namespaces. i18next fetches them all on init. Adding 12+ MSP namespaces means 20+ HTTP requests on every page load. **Solution:** 1. Change `I18N_CONFIG.ns` to only `['common']` 2. Add `ROUTE_NAMESPACES` mapping (route prefix to namespaces): ```typescript // packages/ui/src/lib/i18n/config.ts export const ROUTE_NAMESPACES: Record = { // Client portal routes '/client-portal': ['common', 'client-portal'], '/client-portal/tickets': ['common', 'client-portal', 'features/tickets'], '/client-portal/projects': ['common', 'client-portal', 'features/projects'], '/client-portal/billing': ['common', 'client-portal', 'features/billing'], '/client-portal/documents': ['common', 'client-portal', 'features/documents'], '/client-portal/appointments': ['common', 'client-portal', 'features/appointments'], // MSP portal routes '/msp': ['common', 'msp/core', 'msp/dashboard'], '/msp/tickets': ['common', 'msp/core', 'features/tickets', 'msp/tickets-msp'], '/msp/projects': ['common', 'msp/core', 'features/projects'], '/msp/billing': ['common', 'msp/core', 'features/billing', 'msp/billing'], '/msp/contracts': ['common', 'msp/core', 'msp/contracts'], '/msp/time-management': ['common', 'msp/core', 'msp/time-entry'], '/msp/contacts': ['common', 'msp/core', 'msp/contacts'], '/msp/assets': ['common', 'msp/core', 'msp/assets'], '/msp/dispatch': ['common', 'msp/core', 'msp/dispatch'], '/msp/reports': ['common', 'msp/core', 'msp/reports'], '/msp/settings': ['common', 'msp/core', 'msp/settings'], }; export function getNamespacesForRoute(pathname: string): string[] { // Exact match first if (ROUTE_NAMESPACES[pathname]) return ROUTE_NAMESPACES[pathname]; // Prefix match (e.g., /msp/tickets/123 -> /msp/tickets) const sorted = Object.keys(ROUTE_NAMESPACES).sort((a, b) => b.length - a.length); for (const route of sorted) { if (pathname.startsWith(route)) return ROUTE_NAMESPACES[route]; } return ['common']; } ``` 3. Update `I18nWrapper` to use `usePathname()` and pass route-appropriate namespaces to `I18nProvider` 4. `I18nProvider` calls `i18next.loadNamespaces()` on-demand for the current route ### 0b. Pseudo-Locale Setup Create two test locales for visual QA: - `xx` — all leaf values = `'1111'` - `yy` — all leaf values = `'5555'` **Script:** `scripts/generate-pseudo-locale.ts` ```typescript // Reads any English namespace JSON, outputs pseudo version // - Preserves key structure // - Replaces leaf string values with the fill string // - Preserves {{variables}} within the fill: "1111 {{name}} 1111" // - Preserves pluralization suffixes (_one, _few, _many) // Usage: // npx ts-node scripts/generate-pseudo-locale.ts --locale xx --fill "1111" // npx ts-node scripts/generate-pseudo-locale.ts --locale yy --fill "5555" ``` Add pseudo-locales to config: ```typescript // packages/ui/src/lib/i18n/config.ts const pseudoLocales = ['xx', 'yy'] as const; ``` ### 0c. Formatting Migration Pattern No new utilities needed. The `useFormatters()` hook from `packages/ui/src/lib/i18n/client.tsx` already provides: | Method | Replaces | |--------|----------| | `formatDate(date, opts)` | `new Date(x).toLocaleDateString('en-US', ...)` | | `formatNumber(value, opts)` | `amount.toFixed(2)` for display | | `formatCurrency(value, currency, opts)` | `` `${symbol}${amount.toLocaleString(...)}` `` | | `formatRelativeTime(date)` | Hardcoded `"Today"`, `"Yesterday"` strings | Each batch's checklist includes replacing hardcoded formatting in the affected components. ### 0d. Error/Validation Key Convention Each namespace includes standard sections for errors and messages: ```json { "page": { "title": "..." }, "fields": { "...": "..." }, "actions": { "...": "..." }, "errors": { "loadFailed": "Failed to load data", "saveFailed": "Failed to save changes" }, "validation": { "nameRequired": "Name is required", "emailInvalid": "Please enter a valid email" }, "messages": { "success": { "created": "Successfully created", "updated": "Successfully updated", "deleted": "Successfully deleted" }, "error": { "createFailed": "Failed to create", "updateFailed": "Failed to update" } } } ``` Components use the established client portal pattern — error mapping dictionaries: ```typescript const errorMap: Record = { 'Permission denied': t('errors.permissionDenied'), 'Failed to save': t('errors.saveFailed'), }; const message = errorMap[error.message] || t('errors.unknown'); ``` ### 0e. Split Existing msp.json into msp/core.json Rename `server/public/locales/{lang}/msp.json` to `server/public/locales/{lang}/msp/core.json` for all 7 languages. This namespace (nav, sidebar, header, settings section/tab labels) loads on every MSP route. All other `msp/*` namespaces are route-specific and lazy-loaded. Update any existing references from `useTranslation('msp')` to `useTranslation('msp/core')`. --- ## Phase 1: Translation Batches ### Batch Checklist (Repeatable) Every batch follows this exact process: - [ ] **1. Inventory** — List all components in the feature area. Count hardcoded strings, inline date/number formatting, error/toast messages. Record in PR description. - [ ] **2. Create namespace JSON** — Build `server/public/locales/en/msp/.json` with all extracted keys following the naming convention. - [ ] **3. Extract strings** — Replace hardcoded strings with `t('key')` calls. Add `useTranslation('msp/')` import. - [ ] **4. Migrate formatting** — Replace hardcoded `toLocaleDateString('en-US')`, manual currency formatting, etc. with `useFormatters()` hook. - [ ] **5. Add error/validation keys** — Add error mapping dictionaries in components. Add `errors.*`, `validation.*`, `messages.*` keys to namespace JSON. - [ ] **6. Generate pseudo-locales** — Run `scripts/generate-pseudo-locale.ts` for the new namespace. - [ ] **7. Visual QA** — Switch to `xx` locale, navigate through all pages in the feature area. Anything not showing `1111` is a missed string. Fix and re-test. - [ ] **8. Generate real translations** — Use AI to translate English JSON into all 6 non-English languages. - [ ] **9. Validate** — Run JSON validation script. Check key counts match across all 7 languages. Check no `{{variables}}` are broken. - [ ] **10. Ship** — Commit, PR, merge. Still behind `msp-i18n-enabled` flag. ### Batch Order | Batch | Namespace | Est. Keys | Scope | Notes | |-------|-----------|-----------|-------|-------| | **1** | `msp/settings` | ~300 | All Settings tabs: General, Users, Teams, Ticketing, Projects, Time Entry, Billing, Notifications, Email, Integrations, Extensions, Experimental | Largest surface area. Exercises full workflow. Language settings UI lives here. | | **2** | `msp/dashboard` | ~150 | Dashboard page, command center widgets, quick stats, recent activity | High visibility — first thing users see when flag goes live. | | **3** | `msp/time-entry` | ~200 | Time entry form, timesheet view, approvals, time reports | Core daily workflow. Heavy date/number formatting migration. | | **4** | `msp/billing` | ~200 | MSP-specific billing views, invoice generation, payment tracking | Only MSP-specific parts. Shared billing keys already in `features/billing.json`. Currency formatting heavy. | | **5** | `msp/contracts` | ~200 | Contract management, service agreements, SLAs | Often accessed alongside billing. | | **6** | `msp/tickets-msp` | ~100 | MSP-only ticket views, assignment, SLA tracking, internal notes | Small batch. Shared ticket keys already in `features/tickets.json`. | | **7** | `msp/contacts` | ~100 | Contact and company management, contact details | | | **8** | `msp/assets` | ~100 | Asset management, asset details, asset types | | | **9** | `msp/dispatch` | ~150 | Technician dispatch, scheduling grid | | | **10** | `msp/reports` | ~150 | Reporting module, report builder, export | | | **11** | `msp/admin` | ~200 | Admin panels, tenant configuration, system settings | Lower frequency usage. | | **12** | `msp/workflows` | ~150 | Workflow builder, automation hub, runs, events, dead letter | Lower frequency usage. | **Total estimated new MSP keys: ~2,000** ### Cumulative key count by batch completion | After batch | New keys | Cumulative MSP keys | Total all namespaces | |-------------|----------|---------------------|----------------------| | Infrastructure | 0 | ~60 (core) | ~1,660 | | 1 (settings) | ~300 | ~360 | ~1,960 | | 2 (dashboard) | ~150 | ~510 | ~2,110 | | 3 (time-entry) | ~200 | ~710 | ~2,310 | | 4 (billing) | ~200 | ~910 | ~2,510 | | 5 (contracts) | ~200 | ~1,110 | ~2,710 | | 6 (tickets-msp) | ~100 | ~1,210 | ~2,810 | | 7 (contacts) | ~100 | ~1,310 | ~2,910 | | 8 (assets) | ~100 | ~1,410 | ~3,010 | | 9 (dispatch) | ~150 | ~1,560 | ~3,160 | | 10 (reports) | ~150 | ~1,710 | ~3,310 | | 11 (admin) | ~200 | ~1,910 | ~3,510 | | 12 (workflows) | ~150 | ~2,060 | ~3,660 | --- ## Key Naming Convention All namespaces follow a consistent structure: ```json { "page": { "title": "Page Title", "description": "Page description text" }, "sections": { "sectionName": { "title": "Section Title", "empty": "No items found" } }, "fields": { "fieldName": { "label": "Field Label", "placeholder": "Enter value...", "help": "Help text for this field" } }, "actions": { "create": "Create", "edit": "Edit", "delete": "Delete" }, "table": { "columns": { "name": "Name", "status": "Status", "date": "Date" }, "empty": "No records found" }, "dialogs": { "confirmDelete": { "title": "Confirm Deletion", "message": "Are you sure you want to delete this item?", "confirm": "Delete", "cancel": "Cancel" } }, "errors": { "loadFailed": "Failed to load data", "saveFailed": "Failed to save changes" }, "validation": { "nameRequired": "Name is required" }, "messages": { "success": { "created": "Successfully created", "updated": "Successfully updated" }, "error": { "createFailed": "Failed to create" } } } ``` --- ## Date/Time/Number Formatting Strategy ### Existing infrastructure (no new code needed) | Utility | Location | Locale source | |---------|----------|---------------| | `useFormatters()` hook | `packages/ui/src/lib/i18n/client.tsx` | i18n context (automatic) | | `formatCurrency()` | `packages/core/src/lib/formatters.ts` | Parameter (manual) | | `formatDate()` | `packages/core/src/lib/formatters.ts` | Parameter (manual) | Prefer `useFormatters()` in React components — it reads locale from i18n context automatically. ### Migration table | Current pattern | Replace with | |----------------|-------------| | `new Date(x).toLocaleDateString('en-US', opts)` | `const { formatDate } = useFormatters(); formatDate(x, opts)` | | `new Date(x).toLocaleString(undefined, opts)` | `formatDate(x, opts)` | | `` `${currency}${amount.toLocaleString(...)}` `` | `formatCurrency(amount, currency)` | | `amount.toFixed(2)` (displayed to user) | `formatNumber(amount, { minimumFractionDigits: 2 })` | | Hardcoded `"Today"`, `"Yesterday"` in relative time | `formatRelativeTime(date)` | ### Server-side formatting Server-side utilities in `server/src/lib/utils/dateTimeUtils.ts` stay as-is. They handle timezone conversion and internal date manipulation. User-facing formatting is done client-side via `useFormatters()`. --- ## Error/Validation Translation Strategy Follow the established client portal pattern. No new architecture. ### Server actions Server actions continue returning English error strings in `{ success, error?, data? }` result objects. No server-side i18n required. ### Client components Components map English error strings to translation keys: ```typescript import { useTranslation } from '@alga-psa/ui/lib/i18n/client'; function SettingsPage() { const { t } = useTranslation('msp/settings'); const handleSave = async () => { try { const result = await updateSettings(data); if (result.success) { toast.success(t('messages.success.updated')); } else { const errorMap: Record = { 'Permission denied': t('errors.permissionDenied'), 'Invalid configuration': t('errors.invalidConfig'), }; toast.error(errorMap[result.error!] || t('errors.saveFailed')); } } catch (err) { toast.error(t('errors.unknown')); } }; } ``` ### Validation Client-side validation uses translation keys directly: ```typescript if (!name.trim()) { setFieldError('name', t('validation.nameRequired')); } ``` --- ## Translation Generation Workflow For each batch, after the English namespace JSON is finalized: ### Step 1: AI translation Feed the English JSON to Claude or GPT-4 with this prompt context: - "Translate this JSON for MSP/PSA management software UI" - "Use formal register" - "Preserve all `{{variables}}` exactly as-is" - "Preserve JSON structure and key names (translate values only)" - "Output valid JSON" ### Step 2: Polish-specific rules Polish requires special attention: - **Plural forms:** 1 (`_one`), 2-4 (`_few`), 5+ (`_many`) - **Formal register:** Use "Pan/Pani" forms - **Date format:** DD.MM.YYYY - **Number format:** 1 234,56 (space as thousands, comma as decimal) ```json { "items_one": "{{count}} element", "items_few": "{{count}} elementy", "items_many": "{{count}} elementow" } ``` ### Step 3: Cross-language validation Run validation script after each batch: - All keys present in all 7 languages - No broken `{{variables}}` - Valid JSON (no syntax errors, no duplicate keys) - Key counts match across languages ### Step 4: Human review (optional per batch) Recommended at minimum for: - Polish (primary non-English audience) - One Romance language (French or Spanish) as spot-check ### Translation cost estimate | Item | Cost | |------|------| | AI translation per batch (~200 keys x 6 languages) | ~$40 | | AI translation all 12 batches | ~$240 | | Human review for Polish (all batches) | ~$500-800 | | Human review for one additional language | ~$300-500 | | **Total (AI + Polish review)** | **~$750-1,050** | --- ## Pseudo-Locale Visual QA Process ### Setup (once, during infrastructure sprint) 1. Create `scripts/generate-pseudo-locale.ts` 2. Generate `xx` and `yy` locale files for all existing namespaces 3. Add pseudo-locales to dev config ### Per-batch QA process 1. Run `npx ts-node scripts/generate-pseudo-locale.ts --locale xx --fill "1111"` after creating English namespace 2. Enable `msp-i18n-enabled` flag locally 3. Switch browser locale to `xx` 4. Navigate through every page in the batch's feature area 5. **Every user-visible string should show `1111`** 6. Strings still showing English = missed extraction. Fix and re-test. 7. Check date/time/number values are locale-formatted (not `1111` — these come from `useFormatters()`, not translation keys) ### What pseudo-locales catch | Shows `1111` | Correct behavior | |-------------|-----------------| | All UI labels, headings, buttons | String was extracted | | Toast messages | Error/success keys added | | Validation messages | Validation keys added | | Empty states, placeholders | Often-forgotten strings caught | | Shows English | Problem | |--------------|---------| | Any label, heading, button | Missed string extraction | | Toast or error message | Missing error mapping | | Tooltip, aria-label | Accessibility text missed | | Shows formatted value (date, number) | Correct behavior | |--------------------------------------|-----------------| | "2/18/2026" or "18.02.2026" | Date comes from `useFormatters()` — locale applied | | "$1,234.56" or "1.234,56 $" | Currency from `useFormatters()` | --- ## Namespace Loading by Route (Reference) After infrastructure sprint, each MSP route loads only the namespaces it needs: | Route | Namespaces loaded | |-------|-------------------| | `/msp` | `common`, `msp/core`, `msp/dashboard` | | `/msp/tickets` | `common`, `msp/core`, `features/tickets`, `msp/tickets-msp` | | `/msp/projects` | `common`, `msp/core`, `features/projects` | | `/msp/billing` | `common`, `msp/core`, `features/billing`, `msp/billing` | | `/msp/contracts` | `common`, `msp/core`, `msp/contracts` | | `/msp/time-management` | `common`, `msp/core`, `msp/time-entry` | | `/msp/contacts` | `common`, `msp/core`, `msp/contacts` | | `/msp/assets` | `common`, `msp/core`, `msp/assets` | | `/msp/dispatch` | `common`, `msp/core`, `msp/dispatch` | | `/msp/reports` | `common`, `msp/core`, `msp/reports` | | `/msp/settings` | `common`, `msp/core`, `msp/settings` | | `/msp/documents` | `common`, `msp/core`, `features/documents` | | `/msp/appointments` | `common`, `msp/core`, `features/appointments` | | `/client-portal` | `common`, `client-portal` | | `/client-portal/tickets` | `common`, `client-portal`, `features/tickets` | | `/client-portal/billing` | `common`, `client-portal`, `features/billing` | | `/client-portal/projects` | `common`, `client-portal`, `features/projects` | | `/client-portal/documents` | `common`, `client-portal`, `features/documents` | | `/client-portal/appointments` | `common`, `client-portal`, `features/appointments` | --- ## Rollout Plan ### During batches (flag OFF for all users) - All work ships behind `msp-i18n-enabled` feature flag - MSP portal stays English-only for all users - Zero risk: flag off = no i18n initialization, no namespace loading - Each batch is an independent PR that can be merged without blocking others ### After all batches complete | Step | Action | Duration | |------|--------|----------| | Internal testing | Enable flag for your tenant. Test all MSP features in 2-3 languages. | 1-2 days | | Fix pass | Address layout issues (German/Dutch text overflow), missing keys, formatting bugs | 2-3 days | | Beta | Enable flag for 5-10 opt-in tenants. Gather feedback. | 1 week | | Gradual rollout | 10% -> 50% -> 100% of tenants | 2-3 weeks | | Cleanup | Remove feature flag checks. Remove pseudo-locales. Simplify `useMspTranslation` if used. | 1-2 days | ### Post-rollout maintenance - New features: developers add translation keys as part of feature development - New strings: follow the key naming convention, add to appropriate namespace - New languages: follow the existing translation guide (`.ai/translation/translation-guide.md`) - Validation: CI checks for missing keys, broken JSON, variable mismatches (optional — can add after rollout) --- ## Key File Locations | Purpose | Path | |---------|------| | i18n config | `packages/ui/src/lib/i18n/config.ts` | | I18nProvider | `packages/ui/src/lib/i18n/client.tsx` | | I18nWrapper | `packages/tenancy/src/components/i18n/I18nWrapper.tsx` | | useFormatters hook | `packages/ui/src/lib/i18n/client.tsx` | | Core formatters | `packages/core/src/lib/formatters.ts` | | Feature flags | `server/src/lib/feature-flags/featureFlags.ts` | | MSP layout (standard) | `server/src/app/msp/layout.tsx` + `MspLayoutClient.tsx` | | MSP layout (EE) | `ee/server/src/app/msp/layout.tsx` + `MspLayoutClient.tsx` | | Translation files | `server/public/locales/{lang}/` | | Hierarchical locale | `packages/tenancy/src/actions/locale-actions/getHierarchicalLocale.ts` | | Pseudo-locale generator | `scripts/generate-pseudo-locale.ts` (to be created) | | Phase 1 plan (reference) | `docs/plans/2026-02-12-msp-i18n-phase1/` | | Translation guide | `.ai/translation/translation-guide.md` | | DB migration guide | `.ai/translation/translation-database-migrations.md` | --- ## Success Metrics | Metric | Target | Measurement | |--------|--------|-------------| | MSP translation coverage | 100% of user-visible strings | Pseudo-locale QA (no English visible with `xx` locale) | | Missing key errors in production | 0 | Console monitoring / Sentry | | Namespace load per route | Max 4 files | Route-namespace mapping | | Page load regression | < 50ms additional | Performance monitoring | | Translation quality (Polish) | Human-reviewed | Native speaker sign-off | | All JSON files valid | 100% | Automated validation script |