PSA/ee/docs/plans/2026-02-18-msp-i18n-full-translation-plan.md
Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

23 KiB

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):
// packages/ui/src/lib/i18n/config.ts
export const ROUTE_NAMESPACES: Record<string, string[]> = {
  // 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'];
}
  1. Update I18nWrapper to use usePathname() and pass route-appropriate namespaces to I18nProvider
  2. 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

// 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:

// 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:

{
  "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:

const errorMap: Record<string, string> = {
  '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/<feature>.json with all extracted keys following the naming convention.
  • 3. Extract strings — Replace hardcoded strings with t('key') calls. Add useTranslation('msp/<feature>') 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:

{
  "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:

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<string, string> = {
          '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:

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)
{
  "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