# Scratchpad — MSP Workflows i18n (Batch 2b-9)

> Rolling notes. Append freely. Never silently change PRD scope — if scope shifts, update PRD first, then note the change here.

## 2026-04-18 — Initial scope discovery

### File counts (non-test `.tsx`)
- `ee/server/src/components/workflow-designer/`: **34** (+ ~20 `.ts` helpers, +40 `__tests__/*.test.{ts,tsx}`)
- `ee/server/src/components/workflow-graph/`: **1** (`WorkflowGraph.tsx`)
- `ee/server/src/components/workflow-run-studio/`: **1** (`RunStudioShell.tsx`)
- `ee/packages/workflows/src/components/workflow/`: **12**

**Real total: 48 user-visible components.** The plan's "~104" figure in `MSP_i18n_plan.md` was inflated by test files and pure-TS helpers. Updated accordingly.

### Zero existing wiring confirmed
- `useTranslation`/`useFormatters` grep across all four directories returned **zero matches**.
- `^export const [A-Z_]+(_DISPLAY|_LABELS|_OPTIONS|_MAP)\s*[:=]` pattern: zero matches → no constant-based label maps to delete. The anti-pattern here is **inline option arrays inside components**, not exported constants.

### Inline option arrays found (13 distinct enums)
All identified at `ee/server/src/components/workflow-designer/...`:
- `WorkflowRunList.tsx:86-98` — workflowRunStatus + workflowRunSort
- `WorkflowRunDetails.tsx:175-188, 550` — workflowStepStatus + workflowLogLevel + filter sentinels
- `WorkflowEventList.tsx:73-76` — workflowEventStatus
- `WorkflowAiSchemaSection.tsx:51-64` — workflowAiSchemaType (duplicated array for object-property and array-item selects)
- `WorkflowActionInputSourceMode.tsx:27-28` — workflowInputSourceMode
- `workflowReferenceSelector.tsx:399-403` — workflowReferenceSection (rendered conditionally on model.*.length>0)
- `WorkflowDesigner.tsx:3713-3714, 4724-4725, 6249-6250, 6550-6551, 6614-6615` — workflowTriggerMode + workflowCanvasView + workflowOnError + workflowWaitMode + workflowWaitTiming

### Decision — single namespace
- Use `msp/workflows` (one namespace) instead of splitting into `msp/workflow-designer` + `msp/workflow-runs` + `msp/workflow-tasks`.
- **Why:** one MSP feature area; no current client-portal consumer; a single namespace keeps `ROUTE_NAMESPACES` simple; key count estimate (~1,000-1,500) is well within precedent (msp/clients has 953 keys in one file).
- **How to apply:** every component in the four directories calls `useTranslation('msp/workflows')`. If a task component is ever reused in client portal, consider extracting `features/workflow-tasks` at that time.

### Decision — enum hooks live in `ee/packages/workflows`
- Colocate with the rest of the workflows package:
  - `ee/packages/workflows/src/constants/workflowEnums.ts` — VALUES + LABEL_DEFAULTS for all 13 enums.
  - `ee/packages/workflows/src/hooks/useWorkflowEnumOptions.ts` — `useXOptions()` / `useFormatX()` for each.
- Style follows `packages/billing/src/hooks/useBillingEnumOptions.ts` (LocalizedOption<V> shape, defaultValue fallback).
- **Why:** matches the precedent set by Batch 2b-4 (billing) and Batch 6 (KB); keeps hook and source-of-truth values in the same package.

### Decision — filter sentinels stay in components
- "All statuses" / "All levels" / "All types" rows in filter dropdowns are *not* enum values. They stay as `t('filters.allStatuses')` etc. in the consuming component, prepended to the hook output:
  ```tsx
  const statusOptions = useWorkflowRunStatusOptions();
  const options = [{ value: 'all', label: t('filters.allStatuses') }, ...statusOptions];
  ```
- **Why:** the sentinel is a UI convention, not part of the enum domain; bundling it into the hook would make the hook's return type misleading.

### Decision — `getActivityStatusOptions` out of scope
- Lives in `ee/packages/workflows/src/actions/activity-actions/activityStatusActions.ts`; returns `{ value, label }` tuples from DB rows (activity status labels are tenant data, not UI chrome).
- No migration needed for this batch. Listed as deferred in the enum-labels backlog (F050).

### Decision — Single namespace for tasks vs. splitting
- `ee/packages/workflows/src/components/workflow/Task*.tsx` and `DynamicForm.tsx` are only mounted today under MSP routes. They render inside `/msp/workflow-editor/<id>` (task assignment step rendering) and prospectively in a dedicated inbox page under MSP.
- Keep these in `msp/workflows` for now. Open question #1 in PRD stays "answered no" unless a client-portal consumer appears.

### Backlog / deferred
- `FormExample.tsx`, `TaskInboxExample.tsx`, `ConditionalFormExample.tsx` — demo components. Not mounted in production. Defer unless extraction is trivial.
- Workflow action registry labels — separate initiative (registry API returns labels as data).
- Expression editor (Monaco) vendor UI — out of scope.
- React Flow node body text driven by workflow-definition data — out of scope.
- Dead-letter engine log messages — out of scope (server-side).

### Validation commands
```bash
# Validate translation coverage
node scripts/validate-translations.cjs

# Regenerate pseudo-locales after adding/changing English keys
node scripts/generate-pseudo-locales.cjs

# Enum anti-pattern audit (must return zero after WF-A merges)
rg -n "\{\s*value:\s*['\"][^'\"]+['\"]\s*,\s*label:\s*['\"][A-Z][^'\"]*['\"]" \
  ee/server/src/components/workflow-designer \
  ee/server/src/components/workflow-graph \
  ee/server/src/components/workflow-run-studio \
  ee/packages/workflows/src/components/workflow

# Hardcoded locale formatting (must return zero after WF-B)
rg -n "toLocaleDateString\(['\"]en|toLocaleString\(['\"]en" \
  ee/server/src/components/workflow-designer \
  ee/server/src/components/workflow-run-studio

# Italian accent audit (run after WF-F translations)
grep -n ' e [a-z]\| puo \| gia \| verra \| funzionalita\| necessario' \
  server/public/locales/it/msp/workflows.json
```

### Gotchas
- **Backend status codes.** `RUNNING / SUCCEEDED / FAILED` etc. are persisted verbatim in DB/Temporal. Use the raw value as the translation-key segment (`enums.workflowRunStatus.RUNNING`, not `.running`). Do NOT normalize casing.
- **Feature flag fallback.** When `msp-i18n-enabled` is off, `I18nWrapper` still renders but forces locale='en'. Every `t()` must pass `defaultValue` so flag-off users see the same English copy as today. Missing `defaultValue` = flag-off regression where users see the raw key.
- **Vitest assertions.** ~40 test files in `workflow-designer/__tests__/`. Several assert on English text (`getByText('Running')`). Migrating the status from inline array to hook means those assertions need to change to `getByRole` or to assert on option values rather than labels. Budget extra time in WF-B/WF-C/WF-D.
- **WorkflowDesigner.tsx is ~7k lines.** Plan WF-C carefully — consider splitting extraction by section (toolbar, palette sidebar, properties sidebar, canvas overlay) if the PR gets too big to review in one pass.
- **Namespace mismatch.** If a `t()` call in a workflow component accidentally uses `msp/core` or `msp/settings` instead of `msp/workflows`, the key will resolve if the target namespace happens to have that key, otherwise it renders the raw key. Lint-style regression test (T040) catches these.

### PR breakdown estimate
| PR | Sub-batch | Files touched | Risk |
|----|-----------|---------------|------|
| 1 | WF-A foundation | `config.ts`, `workflowEnums.ts`, `useWorkflowEnumOptions.ts`, 9 new `msp/workflows.json` files, pseudo-locale gen | Low |
| 2 | WF-B run studio + run list | 12 component files, `msp/workflows.json` (en) additions | Medium (Vitest updates) |
| 3 | WF-E task + form | 9 component files | Low |
| 4 | WF-C designer shell | 3 files but WorkflowDesigner is 7k lines | High (review burden) |
| 5 | WF-D designer editors + mapping | 22 component files | Medium |
| 6 | WF-F translations + QA | locale files only | Low (mechanical) |

Target order: WF-A → WF-B+WF-E in parallel → WF-C → WF-D → WF-F.

### Open TODOs
- [ ] Confirm with design that German "Workflow-Abläufe" / "Arbeitsabläufe" pick in core.json before WF-F translates the workflows namespace.
- [ ] Determine whether `ExpressionEditor` hover/completion messages are user-facing or dev-only (blocks part of F028).
- [ ] Decide whether to write the audit test (T040) as a unit test or extend the existing `ContractLinesSubbatch.i18n.test.ts` pattern.
- [ ] Confirm no active workflow-run-studio redesign is in flight that would conflict with this batch's shell changes.

### Links
- Main plan: `.ai/translation/MSP_i18n_plan.md`
- Translation guide: `.ai/translation/translation-guide.md`
- Enum pattern (with backlog this batch should update): `.ai/translation/enum-labels-pattern.md`
- File-structure reference (update after WF-F): `.ai/translation/translation_files_structure.md`
- Example hook style: `packages/billing/src/hooks/useBillingEnumOptions.ts`
- Example batch SCRATCHPAD style: `ee/docs/plans/2026-04-09-msp-i18n-credits/SCRATCHPAD.md`

## 2026-04-19 — Progress log

### F001 complete — namespace scaffold created
- Added `server/public/locales/{en,fr,es,de,nl,it,pl,xx,yy}/msp/workflows.json`.
- Initial scaffold currently includes `page`, `nav`, `sections`, `empty`, and `actions` roots so downstream component work can add keys incrementally without first creating the namespace.
- Non-English production locales currently copy English as temporary stubs, matching the PRD. `xx` and `yy` were also created as temporary structural stubs so `validate-translations.cjs` passes from the first commit; F041 will regenerate them from English later.
- Validation run: `node scripts/validate-translations.cjs` — passed with 0 missing/extra keys across production locales and structural match for pseudo-locales.

### F002 complete — route namespace loading wired
- Added `msp/workflows` namespace loading to `packages/core/src/lib/i18n/config.ts` for:
  - `/msp/workflows`
  - `/msp/workflows/runs`
  - `/msp/workflow-editor`
  - `/msp/workflow-control`
- Verified longest-prefix behavior with:
  ```bash
  node --import tsx/esm -e "import { ROUTE_NAMESPACES, getNamespacesForRoute } from './packages/core/src/lib/i18n/config.ts'; ..."
  ```
- Confirmed dynamic workflow routes now resolve to `['common', 'msp/core', 'msp/workflows']`, including:
  - `/msp/workflows/runs/run-123`
  - `/msp/workflow-editor/abc`
  - `/msp/workflow-editor/new`

### F003 complete — workflow enum source of truth added
- Added `ee/packages/workflows/src/constants/workflowEnums.ts`.
- The file now exports 13 `*_VALUES` arrays plus 13 `*_LABEL_DEFAULTS` records for:
  - run status
  - run sort
  - event status
  - step status
  - log level
  - AI schema type
  - input source mode
  - reference section
  - trigger mode
  - canvas view
  - on-error
  - wait mode
  - wait timing
- Preserved raw backend values exactly as they appear today (`RUNNING`, `RETRY_SCHEDULED`, `started_at:desc`, etc.) so translation keys can be built directly from persisted values without normalization.
- Validation command:
  ```bash
  npx tsx -e "import * as enums from './ee/packages/workflows/src/constants/workflowEnums.ts'; ..."
  ```
  Verified every label-default map contains an entry for every enum value.

### F004 complete — localized enum hooks added
- Added `ee/packages/workflows/src/hooks/useWorkflowEnumOptions.ts`.
- Exported `useXOptions()` and `useFormatX()` hooks for all 13 workflow enums, all bound to `useTranslation('msp/workflows')`.
- Each hook uses `defaultValue` from the corresponding `*_LABEL_DEFAULTS` record so:
  - the workflow UI stays readable before the namespace finishes loading
  - flag-off MSP users still see English instead of raw translation keys
  - unknown server values fall back to the raw value string in formatter hooks
- Updated `ee/packages/workflows/package.json` to expose:
  - `@alga-psa/workflows/hooks/*`
  - `@alga-psa/workflows/constants/*`
- Sanity check:
  ```bash
  npx tsx -e "import { useWorkflowRunStatusOptions, useFormatWorkflowRunStatus } from './ee/packages/workflows/src/hooks/useWorkflowEnumOptions.ts'; ..."
  ```
  Confirmed the new hook module resolves and exports functions.

### F005 complete — enum translation keys seeded
- Added `enums.*` trees for all 13 workflow enums under `server/public/locales/en/msp/workflows.json`.
- To keep `validate-translations.cjs` green while the feature batch is still in progress, mirrored the same key structure into `fr/es/de/nl/it/pl/xx/yy` as temporary English stubs. F042/F041 will replace those with real translations and regenerated pseudo-locales later.
- Validation rerun after the enum-key expansion:
  ```bash
  node scripts/validate-translations.cjs
  ```
  Passed with 0 missing/extra keys.

### F006 complete — WorkflowRunList uses enum hooks
- Updated `ee/server/src/components/workflow-designer/WorkflowRunList.tsx` to consume:
  - `useWorkflowRunStatusOptions()`
  - `useWorkflowRunSortOptions()`
- Removed the local `STATUS_OPTIONS` and `SORT_OPTIONS` inline English arrays.
- Kept the filter sentinel local to the component per PRD guidance:
  - `t('filters.allStatuses', { defaultValue: 'All statuses' })`
- Added `filters.allStatuses` to `msp/workflows.json` across all locales as a temporary English stub.
- Checks run:
  ```bash
  npx eslint ee/server/src/components/workflow-designer/WorkflowRunList.tsx
  node scripts/validate-translations.cjs
  ```
- ESLint result: no errors; existing warnings remain in `WorkflowRunList.tsx` for pre-existing `any`/unused-variable sites unrelated to this enum migration.

### F007 complete — WorkflowRunDetails uses enum hooks
- Updated `ee/server/src/components/workflow-designer/WorkflowRunDetails.tsx` to consume:
  - `useWorkflowStepStatusOptions()`
  - `useWorkflowLogLevelOptions()`
- Removed the local inline arrays for step-status and log-level filters.
- Localized the remaining filter sentinels with `t()`:
  - `filters.allStatuses`
  - `filters.allLevels`
  - `filters.allTypes`
- `nodeTypeOptions` still builds dynamic type values from the loaded workflow definition, but the sentinel row is now translated.
- Added `filters.allLevels` and `filters.allTypes` to every locale file as temporary English stubs.
- Checks run:
  ```bash
  npx eslint ee/server/src/components/workflow-designer/WorkflowRunDetails.tsx
  node scripts/validate-translations.cjs
  ```
- ESLint result: no errors; current warnings in `WorkflowRunDetails.tsx` are pre-existing `any`/unused/non-null-assertion warnings outside this enum-hook change.

### F008 complete — WorkflowEventList uses enum hooks
- Updated `ee/server/src/components/workflow-designer/WorkflowEventList.tsx` to consume `useWorkflowEventStatusOptions()`.
- Removed the local inline event-status options array.
- Reused `filters.allStatuses` for the sentinel row.
- Checks run:
  ```bash
  npx eslint ee/server/src/components/workflow-designer/WorkflowEventList.tsx
  node scripts/validate-translations.cjs
  ```
- ESLint result: no errors; existing warnings are unrelated pre-existing `any`/unused-variable warnings in `WorkflowEventList.tsx`.

### F009 complete — WorkflowAiSchemaSection uses schema-type hook
- Updated `ee/server/src/components/workflow-designer/WorkflowAiSchemaSection.tsx` to consume `useWorkflowAiSchemaTypeOptions()`.
- Removed the duplicated hardcoded primitive-type arrays.
- Preserved current UX by filtering out the `array` option only for the array-item picker, while the main field-type selector still exposes all 6 enum values from the shared hook.
- Checks run:
  ```bash
  npx eslint ee/server/src/components/workflow-designer/WorkflowAiSchemaSection.tsx
  node scripts/validate-translations.cjs
  ```
- Result: no ESLint errors or warnings from this file; translation validation remained green.

### F010 complete — WorkflowActionInputSourceMode uses enum hook
- Updated `ee/server/src/components/workflow-designer/WorkflowActionInputSourceMode.tsx` to consume `useWorkflowInputSourceModeOptions()`.
- Removed the local `SOURCE_MODE_OPTIONS` inline English array.
- Kept all source-mode derivation / transition logic unchanged; only the select-label source changed.
- Checks run:
  ```bash
  npx eslint ee/server/src/components/workflow-designer/WorkflowActionInputSourceMode.tsx
  node scripts/validate-translations.cjs
  ```
- Result: no ESLint errors or warnings from this file; translation validation remained green.

### F011 complete — workflowReferenceSelector uses enum hook
- Updated `ee/server/src/components/workflow-designer/workflowReferenceSelector.tsx` to consume `useWorkflowReferenceSectionOptions()`.
- Preserved the existing visibility rule by filtering the hook output based on whether `payload`, `vars`, `meta`, `error`, or `forEach` actually has entries in the current model.
- Checks run:
  ```bash
  npx eslint ee/server/src/components/workflow-designer/workflowReferenceSelector.tsx
  node scripts/validate-translations.cjs
  ```
- Result: no ESLint errors or warnings from this file; translation validation remained green.

### F012 complete — WorkflowDesigner inline enum arrays removed
- Updated `ee/server/src/components/workflow-designer/WorkflowDesigner.tsx` to consume:
  - `useWorkflowTriggerModeOptions()`
  - `useWorkflowCanvasViewOptions()`
  - `useWorkflowOnErrorOptions()`
  - `useWorkflowWaitModeOptions()`
  - `useWorkflowWaitTimingOptions()`
- Replaced the five inline option arrays called out in the PRD:
  - trigger mode
  - canvas view
  - foreach/on-item-error
  - wait mode
  - wait timing
- Kept the hook calls local to the two component boundaries already present in the file:
  - main `WorkflowDesigner`
  - `StepConfigPanel`
- Checks run:
  ```bash
  npx eslint ee/server/src/components/workflow-designer/WorkflowDesigner.tsx
  node scripts/validate-translations.cjs
  ```
- ESLint result: no errors; the file still has a large pre-existing warning backlog (`unused`, `any`, `react-hooks/exhaustive-deps`, etc.) unrelated to this enum-hook conversion.

### F013 complete — WorkflowRunList strings extracted
- Updated `ee/server/src/components/workflow-designer/WorkflowRunList.tsx` to route all component-owned copy through `useTranslation('msp/workflows')`, including:
  - quick range chips
  - summary strip labels
  - filter labels/placeholders
  - table headers / empty states
  - row action labels
  - bulk-action dialog copy
  - toast fallbacks for load/export/bulk-action flows
- Switched run-status badges and summary counts from raw persisted values (`RUNNING`, `FAILED`, etc.) to `useFormatWorkflowRunStatus()` so localized labels render while action payloads still send raw enum values.
- Added `runList.*` keys to `server/public/locales/{en,fr,es,de,nl,it,pl,xx,yy}/msp/workflows.json` as temporary English stubs; real translations remain WF-F work.
- Checks run:
  ```bash
  npx eslint ee/server/src/components/workflow-designer/WorkflowRunList.tsx
  node scripts/validate-translations.cjs
  ```
- ESLint result: no errors; the remaining warnings in `WorkflowRunList.tsx` are pre-existing `any`/unused-variable sites, plus the same existing helper-input typing.

### Added backlog item — workflowRunTriggerPresentation helper still returns English
- While extracting `WorkflowRunList.tsx`, confirmed `ee/server/src/components/workflow-designer/workflowRunTriggerPresentation.ts` still returns hardcoded English labels for:
  - `Manual`
  - `Event`
  - `One-time schedule`
  - `Recurring schedule`
  - schedule statuses such as `Scheduled` / `Paused`
- This helper is explicitly named in the PRD WF-B surface list but was missing from `features.json`.
- Added:
  - `F051` — translate the helper via workflow-aware formatters
  - `T046` — formatter coverage test for trigger + schedule-status labels
- Rationale: without a dedicated item, run-list and dialog surfaces would still leak English even after component extraction work is complete.

### F014 complete — WorkflowRunDetails strings extracted
- Updated `ee/server/src/components/workflow-designer/WorkflowRunDetails.tsx` to localize component-owned copy across:
  - run header + action bar
  - summary metadata grid
  - step timeline filters / table / empty state
  - step detail panels, wait history, envelope tabs
  - action invocation cards
  - log viewer and audit trail
  - all five confirmation dialogs
  - toast fallbacks for load/export/retry/resume/cancel/replay/requeue flows
- Switched workflow run / step / log level badges from raw enum values to:
  - `useFormatWorkflowRunStatus()`
  - `useFormatWorkflowStepStatus()`
  - `useFormatWorkflowLogLevel()`
- Added `runDetails.*` keys to `server/public/locales/en/msp/workflows.json`, then synced the same stub structure into `fr/es/de/nl/it/pl/xx/yy` to preserve validation parity until WF-F translations.
- Checks run:
  ```bash
  npx eslint ee/server/src/components/workflow-designer/WorkflowRunDetails.tsx
  node scripts/validate-translations.cjs
  ```
- ESLint result: no errors; remaining warnings are the file's pre-existing `any`/unused-type/non-null-assertion backlog unrelated to the i18n extraction.

### F015 complete — WorkflowRunDialog strings extracted
- Updated `ee/server/src/components/workflow-designer/WorkflowRunDialog.tsx` to use `useTranslation('msp/workflows')` for component-owned copy across:
  - dialog title / description / footer actions
  - workflow/event/schema selectors
  - draft/system/concurrency warnings
  - payload builder controls and validation summary
  - preset management and clipboard/latest-run toasts
  - form-builder object/array helper controls (`Show/Hide`, `Add field`, `Remove`, etc.)
- Added `runDialog.*` keys to `server/public/locales/en/msp/workflows.json`, then synced the same stub content into `fr/es/de/nl/it/pl/xx/yy`.
- Sanity check run:
  ```bash
  node - <<'NODE'
  # compared all runDialog.* keys referenced in WorkflowRunDialog.tsx against en/msp/workflows.json
  NODE
  ```
  Result: zero missing keys.
- Checks run:
  ```bash
  npx eslint ee/server/src/components/workflow-designer/WorkflowRunDialog.tsx
  node scripts/validate-translations.cjs
  ```
- ESLint result: no errors; remaining warnings are pre-existing `any` / unused-prop / hooks-backlog warnings in the dialog file.

### F016 complete — WorkflowEventList strings extracted
- Updated `ee/server/src/components/workflow-designer/WorkflowEventList.tsx` to localize:
  - summary badges
  - filter labels/placeholders
  - table column headers
  - empty/loading states
  - event-detail drawer labels and action copy
  - export/detail-load toast fallbacks
- Switched event-status badges to `useFormatWorkflowEventStatus()` so `matched` / `unmatched` / `error` no longer render as inline English casing logic.
- Added `eventList.*` keys to `server/public/locales/en/msp/workflows.json`, then synced the stub structure into `fr/es/de/nl/it/pl/xx/yy`.
- Sanity check run:
  ```bash
  node - <<'NODE'
  # compared all eventList.* keys referenced in WorkflowEventList.tsx against en/msp/workflows.json
  NODE
  ```
  Result: zero missing keys.
- Checks run:
  ```bash
  npx eslint ee/server/src/components/workflow-designer/WorkflowEventList.tsx
  node scripts/validate-translations.cjs
  ```
- ESLint result: no errors; remaining warnings are limited to pre-existing `any` / unused-catch-variable sites.

### F017 complete — WorkflowDeadLetterQueue strings extracted
- Updated `ee/server/src/components/workflow-designer/WorkflowDeadLetterQueue.tsx` to localize:
  - minimum-retry filter label/placeholder
  - table column headers
  - loading/empty states
  - refresh/load-more actions
  - dead-letter load toast fallback
- Switched dead-letter run status badges to `useFormatWorkflowRunStatus()` so persisted status codes no longer render raw.
- Added `deadLetter.*` keys to `server/public/locales/en/msp/workflows.json`, then synced the same stub structure into `fr/es/de/nl/it/pl/xx/yy`.
- Sanity check run:
  ```bash
  node - <<'NODE'
  # compared all deadLetter.* keys referenced in WorkflowDeadLetterQueue.tsx against en/msp/workflows.json
  NODE
  ```
  Result: zero missing keys.
- Checks run:
  ```bash
  npx eslint ee/server/src/components/workflow-designer/WorkflowDeadLetterQueue.tsx
  node scripts/validate-translations.cjs
  ```
- ESLint result: clean, no warnings or errors from this file after the extraction.

### F018 complete — schedules surface extracted in automation-hub package
- `ee/server/src/components/workflow-designer/WorkflowSchedules.tsx` is only a wrapper; the actual schedule list and dialog live in:
  - `ee/packages/workflows/src/components/automation-hub/Schedules.tsx`
  - `ee/packages/workflows/src/components/automation-hub/WorkflowScheduleDialog.tsx`
  - `ee/packages/workflows/src/components/automation-hub/WorkflowScheduleTimezonePicker.tsx`
  - `ee/packages/workflows/src/components/automation-hub/workflowScheduleRecurrence.ts`
- Localized the full schedules surface under `schedules.*` in `msp/workflows.json`, covering:
  - list heading, filters, table columns, statuses, row actions, empty/loading/error states
  - create/edit dialog title, fields, recurring builder copy, business-hours guidance, payload editor chrome, validation copy
  - timezone-picker browse/custom affordances
  - recurrence summary/validation text via localization-aware helper options instead of hardcoded English strings
- Switched schedules timestamp rendering to locale-aware client formatters where practical:
  - list timestamps now use `useFormatters().formatDate(...)`
  - relative timestamps use `useFormatters().formatRelativeTime(...)`
- Kept fr/es/de/nl/it/pl/xx/yy as English structural stubs for this step so translation validation stays green until WF-F.
- Test harness updates:
  - mocked `@alga-psa/ui/lib/i18n/client` with stable `t()` + formatter functions in `Schedules.test.tsx`
  - added the newly consumed `listWorkflowSchemaRefsAction` to the workflow-actions test mock
  - mocked `Dialog` footer and lightweight `TimePicker` / `DateTimePicker` components so the schedule dialog remains testable after the i18n wiring
- Checks run:
  ```bash
  npx vitest run src/components/automation-hub/Schedules.test.tsx src/components/automation-hub/workflowScheduleRecurrence.test.ts
  npx eslint ee/packages/workflows/src/components/automation-hub/Schedules.tsx ee/packages/workflows/src/components/automation-hub/WorkflowScheduleDialog.tsx
  npx eslint ee/packages/workflows/src/components/automation-hub/WorkflowScheduleTimezonePicker.tsx ee/packages/workflows/src/components/automation-hub/workflowScheduleRecurrence.ts ee/packages/workflows/src/components/automation-hub/Schedules.test.tsx
  node scripts/validate-translations.cjs
  ```
- Results:
  - Vitest passed: 30/30 tests across schedules + recurrence
  - translation validation passed with 0 missing/extra keys
  - ESLint reported only pre-existing warnings in `WorkflowScheduleDialog.tsx` (`no-explicit-any`) and the existing warning backlog in `Schedules.test.tsx`

### F019 complete — WorkflowDefinitionAudit strings extracted
- Updated `ee/server/src/components/workflow-designer/WorkflowDefinitionAudit.tsx` to use `useTranslation('msp/workflows')` for:
  - empty/select-workflow state
  - audit card heading
  - export/load-more actions
  - table column headers
  - system/empty-value fallbacks
  - empty table state
  - load/export toast fallbacks
- Added `audit.*` keys to `server/public/locales/{en,fr,es,de,nl,it,pl,xx,yy}/msp/workflows.json` as English stubs pending WF-F translation work.
- Deliberately left the timestamp formatter itself for `F022`, which is the plan item dedicated to locale-aware date formatting in the audit/run surfaces.
- Checks run:
  ```bash
  npx eslint ee/server/src/components/workflow-designer/WorkflowDefinitionAudit.tsx
  node scripts/validate-translations.cjs
  ```
- Results:
  - ESLint clean
  - translation validation passed with 0 missing/extra keys

### F020 complete — RunStudioShell strings extracted
- Updated `ee/server/src/components/workflow-run-studio/RunStudioShell.tsx` to use `useTranslation('msp/workflows')` for component-owned copy across:
  - header kicker/title/version/updated badges and back-to-workflows link
  - replay/cancel action buttons
  - run-status indicator row and pipeline view toggle (Graph/List)
  - execution-pipeline empty/loading/no-steps states and step-card labels (if/loop/try/block, then/else/try/catch/body sections, forEach summary)
  - step-status badges (running/succeeded/failed/retrying/pending/canceled) with attempt counter
  - run-details card fields (run id, started, duration, tenant, trigger, event type, schedule state, scheduled for, cron, waiting for, counts)
  - run errors panel and step-details empty/panels (configuration, input resolved, output, envelope snapshot)
  - execution timeline (search, empty, attempt/wait entries, status/event/key segments, created/resolved lines, jump buttons)
  - run logs (search, filters, clear, empty state) with localized log-level button labels via `useFormatWorkflowLogLevel()`
  - cancel/replay dialog (title, heading, description, reason/payload fields, close/confirm/working actions, invalid-JSON error)
  - toast fallbacks for reason-required/canceled/replay-started/action-failed flows
- Switched the run-status badge from raw persisted values to `useFormatWorkflowRunStatus()` while keeping `statusBadgeClasses` style lookups on the raw status code.
- Added `runStudio.*` keys to `server/public/locales/en/msp/workflows.json` (168 keys) and synced the same stub structure into `fr/es/de/nl/it/pl/xx/yy`.
- Cross-checked every `t('runStudio.*')` key referenced in the component against `en/msp/workflows.json`:
  - 104 keys referenced, 0 missing.
- Fixed a small indentation inconsistency in the `getStepStatusStyle` default branch introduced during extraction.
- Checks run:
  ```bash
  npx eslint ee/server/src/components/workflow-run-studio/RunStudioShell.tsx
  node scripts/validate-translations.cjs
  ```
- Results:
  - ESLint reports 0 errors; remaining warnings are pre-existing `no-empty`, `exhaustive-deps`, non-null-assertion, and `any` sites unrelated to the i18n extraction.
  - translation validation passed with 0 missing/extra keys.
- Deliberately left `new Date(...).toLocaleString()` calls (`started`, `scheduled for`, `created`, `resolved`, timeline entry created/resolved lines) for `F022`, and `getWorkflowRunTriggerLabel`/`getWorkflowScheduleStatusLabel` helper output for `F051`.

### F021 complete — WorkflowGraph chrome strings extracted
- Updated `ee/server/src/components/workflow-graph/WorkflowGraph.tsx` to use `useTranslation('msp/workflows')` inside the four render-level node components and the main export:
  - `StartNode` — translated start-node label via `graph.start.label`, falling back to the incoming `data.label` for non-localized callers.
  - `StepNode` — translated the input-mapping badge/title (`{{count}} req unmapped` and `{{count}} required fields unmapped`), the "All required fields mapped" tooltip/aria-label, and the "Delete step" button title/aria-label.
  - `InsertNode` — translated the "Drop a step here to insert" droppable title.
  - `WorkflowGraph` body — translated the "Building graph…" loading state, the "Graph render error" + "Switch to List view to continue editing." build-error card, the readonly empty-state message, and both droppable empty-state messages ("Drop to add as the first step" and "Drag a step from the panel…").
- Added `graph.*` keys to `server/public/locales/en/msp/workflows.json` (13 keys covering start, states, errors, empty, mapping, insert, actions) and synced the same stub structure into `fr/es/de/nl/it/pl/xx/yy`.
- Cross-checked every `t('graph.*')` key referenced in the component against `en/msp/workflows.json`:
  - 13 keys referenced, 0 missing.
- Backlog: `buildWorkflowGraph.ts` still hard-codes the `next` loop-back edge label. Internal sentinels (`Start`, `Join`, `Done`) in that helper are purely comparison values not shown to users (they drive the ✓/⋯ glyph choice), so left as-is. Edge `next` label is user-visible on the canvas — file a follow-up item to make it translation-aware by threading a label override through `buildWorkflowGraph` options.

### F051 complete — workflow run trigger presentation helpers localized
- Added `ee/server/src/components/workflow-designer/useWorkflowRunTriggerPresentation.ts` with two React hooks colocated with the pure helper:
  - `useFormatWorkflowRunTrigger()` — returns a `(triggerType, eventType?) => string` formatter.
  - `useFormatWorkflowScheduleStatus()` — returns a `(status) => string` formatter.
- Both hooks read from the `msp/workflows` namespace under `trigger.*` and `scheduleStatus.*`, and use `defaultValue` fallbacks so flag-off users still see the same English copy as today.
- Left the pure helpers (`getWorkflowRunTriggerLabel`, `getWorkflowScheduleStatusLabel`, `getWorkflowScheduleStatusBadgeClass`, `isTimeTriggeredRun`) in place so the existing unit test stays authoritative and `getWorkflowScheduleStatusBadgeClass` keeps its single-responsibility class-only API.
- Updated callers to use the new hooks:
  - `WorkflowRunList.tsx` — `workflowTriggerMap` useMemo and the inline row badges.
  - `WorkflowRunDetails.tsx` — `triggerLabel` and schedule-state badge label.
  - `RunStudioShell.tsx` — `triggerLabel` useMemo and schedule-state badge label.
- Added `trigger.*` (5 keys) and `scheduleStatus.*` (6 keys) to `server/public/locales/en/msp/workflows.json`, synced into `fr/es/de/nl/it/pl/xx/yy` as English stubs pending WF-F translation.
- Checks run:
  ```bash
  npx eslint ee/server/src/components/workflow-designer/useWorkflowRunTriggerPresentation.ts ee/server/src/components/workflow-designer/WorkflowRunList.tsx ee/server/src/components/workflow-designer/WorkflowRunDetails.tsx ee/server/src/components/workflow-run-studio/RunStudioShell.tsx
  npx vitest run src/__tests__/unit/workflowRunTriggerPresentation.unit.test.ts
  node scripts/validate-translations.cjs
  ```
- Results:
  - ESLint reports 0 errors; remaining warnings are the pre-existing `any`/non-null-assertion/`exhaustive-deps`/`no-empty`/`no-unused-vars` backlog, unchanged by this extraction.
  - Vitest: 2/2 existing trigger-presentation unit tests still pass (they exercise the pure helpers, not the hooks).
  - translation validation passed with 0 missing/extra keys.

### F022 complete — date formatting switched to useFormatters().formatDate
- Replaced module-level `formatDateTime(value)` helpers in the 5 run/audit/event/DLQ surfaces with component-scoped hooks backed by `useFormatters().formatDate`. Each hook keeps the same `'—'`-on-empty + original-string-on-NaN fallback contract as before:
  - `WorkflowRunList.tsx`
  - `WorkflowRunDetails.tsx`
  - `WorkflowEventList.tsx`
  - `WorkflowDefinitionAudit.tsx`
  - `WorkflowDeadLetterQueue.tsx`
- In `RunStudioShell.tsx`, consolidated all seven inline `new Date(...).toLocaleString()` / `toLocaleTimeString()` call sites onto two component-scoped formatters (`formatDateTime` and `formatTimeOnly`) built from `useFormatters().formatDate`:
  - step-card `title` timestamp, header "Updated {{time}}" badge, run-details `started_at`, scheduled-for metadata, timeline created/resolved lines, and the log timestamp column.
- All replacements pass `{ dateStyle: 'medium', timeStyle: 'short' }` for full datetimes and `{ timeStyle: 'medium' }` for time-only, so the locale context drives ordering / 12h-vs-24h / weekday spelling consistently instead of the browser default.
- Note: `ee/packages/workflows/src/components/automation-hub/Schedules.tsx` was already migrated to `useFormatters().formatDate` as part of F018.
- Checks run:
  ```bash
  npx eslint ee/server/src/components/workflow-designer/WorkflowRunList.tsx ee/server/src/components/workflow-designer/WorkflowRunDetails.tsx ee/server/src/components/workflow-designer/WorkflowDefinitionAudit.tsx ee/server/src/components/workflow-designer/WorkflowEventList.tsx ee/server/src/components/workflow-designer/WorkflowDeadLetterQueue.tsx ee/server/src/components/workflow-run-studio/RunStudioShell.tsx
  grep -rn "toLocaleDateString\|toLocaleString\|toLocaleTimeString" ee/server/src/components/workflow-designer ee/server/src/components/workflow-run-studio --include='*.tsx' --include='*.ts'
  node scripts/validate-translations.cjs
  ```
- Results:
  - ESLint reports 0 errors across all 6 files; remaining warnings are the pre-existing `any`/`no-empty`/non-null-assertion/`exhaustive-deps` backlog.
  - `grep` for locale-sensitive Date method calls returns zero matches across the workflow-designer and workflow-run-studio directories.
  - translation validation passed with 0 missing/extra keys.

### F023 complete — WorkflowDesigner shell strings extracted
- Scope limited to shell chrome per PRD WF-C: header/toolbar/page title & description, validation badge + tooltip, status dialogs, tabs, block-level step-card chrome, and top-level toasts. Inner components that belong to F024–F034 (palette, StepConfigPanel properties sidebar, mapping editor, expression editor, AI schema section, compose text, step-level editor fields) were intentionally left untranslated in this pass.
- Added `useTranslation('msp/workflows')` to two places in this ~7.5k-line file:
  - Main `WorkflowDesigner` component (top of function body).
  - `StepCard` inner component (used by both root and block pipelines inside the designer surface).
- Localized in `WorkflowDesigner`:
  - Control-panel tab labels (`Schedules` / `Runs` / `Events` / `Event Catalog` / `Dead Letter`).
  - Page title + description for all three modes (`control-panel`, `editor-designer`, `editor-list`).
  - Toolbar: `Back to workflows` link, `New Workflow`, `Save Draft` + `Saving…`, `Publish` + `Publishing…`, `Run`, and the run-disabled "Preview only until a version is published." tooltip.
  - Validation status badge (`Invalid` / `Warnings` / `Valid` / `Unknown`) and the header tooltip (`Last validated: …` / `Validation status unknown`).
  - Two confirmation dialogs: discard-changes and event-schema-adoption (title, message, confirm, cancel — including parameterized message with `eventName`/`schemaRef`).
  - Trigger-label passed into `WorkflowRunDialog` now routes through the shared `trigger.*` keys seeded by F051 (`Event: {{eventType}}`, `One-time schedule`, `Recurring schedule`, `Manual`).
  - Scattered toasts: load registries/permissions/workflows/event catalog failures, settings update success + failure, save/create/publish success + failure, save-before-publish error, publish validation-errors warning, and the system-event missing-schema warning.
- Localized in `StepCard`:
  - Card select-button aria-label (`Select {{label}} step`).
  - Control-block badges (`If` / `Loop` / `Try` / `Block`).
  - Input-mapping status badge (`{{count}} required unmapped`) and counterpart all-mapped tooltip + aria-label.
  - Duplicate + delete tooltip / aria-labels and the error-count badge (with singular/plural pieces).
  - `forEach` summary line (`Item: {{itemVar}} | Concurrency: {{concurrency}}`).
- `BlockSection` titles (`THEN` / `ELSE` / `TRY` / `CATCH` / `BODY`) flow through `t()` at the call sites (inside `StepCard`) so the inner `BlockSection` component stays a passive presentational wrapper.
- Added 73 keys under `designer.*` in `server/public/locales/en/msp/workflows.json`, synced the same stub structure into `fr/es/de/nl/it/pl/xx/yy` pending WF-F translation work. The `trigger.*` keys added for F051 are reused here rather than duplicated.
- Deliberately deferred:
  - `StepConfigPanel` properties sidebar copy (save-as validation banner, inline references section, etc.) — belongs to F029.
  - `Pipe` component drop-hint / empty copy — none found in current code beyond step-card and block-section.
  - Toast fallback at line 5958 (`Copied: {{path}}`) — lives inside `StepConfigPanel`, scoped with F029.
  - The extensive inline validation-error and trigger-validation copy surfaced by the roadmap; those render error payloads from the server and are better handled as part of F040's server-error mapping pass.
- Cross-checked every `t('designer.*')` key referenced in the component against `en/msp/workflows.json`:
  - 73 keys referenced, 0 missing.
- Checks run:
  ```bash
  npx eslint ee/server/src/components/workflow-designer/WorkflowDesigner.tsx
  node scripts/validate-translations.cjs
  ```
- Results:
  - ESLint reports 0 errors; remaining warnings are the file's long-standing `any` / non-null-assertion / `no-unused-vars` / `react/no-unescaped-entities` / `exhaustive-deps` backlog, unchanged by this extraction.
  - translation validation passed with 0 missing/extra keys.

### F024 + F025 complete — palette chrome + control-block tiles localized
- `WorkflowDesignerPalette.tsx` is now translation-aware via `useTranslation('msp/workflows')`. Localized:
  - `Show palette` / `Hide palette` labels on the collapse toggle.
  - `Search` placeholder on the search input.
  - `Drop on pipeline to add` hint shown while dragging.
  - Category headers — the component now calls `t(\`designer.palette.categories.${category}\`, { defaultValue: category })`, so the hardcoded `'Core' | 'Transform' | 'AI' | 'Apps' | 'Control'` keys produced upstream translate when matching locale keys exist and fall back to the raw English label otherwise.
- `PaletteItemWithTooltip.tsx` has no hardcoded user-visible strings — tooltip label/description flow entirely through the `item: PaletteTooltipItem` prop. F025's "extraction" therefore happens at the caller site (this batch) and at any future catalog source that feeds the palette (deferred).
- `CONTROL_BLOCKS` in `WorkflowDesigner.tsx` are now translated at mapping time inside `paletteItems`: each block's label and description resolve via `t(\`designer.palette.controlBlocks.${block.id}.label\`, …)` / `...description`, with the hardcoded English retained as `defaultValue`. Both the translated and original strings stay in the palette search index so in-flight translations don't break `buildPaletteSearchIndex`/`matchesPaletteSearchQuery` matching for either surface.
- Added `designer.palette.*` keys to `server/public/locales/en/msp/workflows.json` (17 keys: chrome, 5 category labels, 5 control-block label+description pairs) and synced the same stub structure into `fr/es/de/nl/it/pl/xx/yy`.
- Existing vitest for the palette passes: ran `src/components/workflow-designer/__tests__/WorkflowDesignerPalette.test.tsx` → 2/2 tests pass. `useTranslation` emits a no-i18next-instance log in test context as expected; fallbacks to `defaultValue` keep the English assertions (`getByPlaceholderText('Search')`, `getByText('Drop on pipeline to add')`) green.
- Deferred:
  - Action registry / designer catalog items coming from the server — their `label` / `description` / `groupLabel` values are server-sourced palette data, covered by the workflow action registry initiative (out of scope for this batch per PRD).
  - `outputSummary` default string `'Choose an action after adding this step'` lives in `WorkflowDesigner.tsx` inside `groupedActionItems` mapping; this is palette tile copy tied to the registry flow and is better bundled with the registry-labels initiative.
- Checks run:
  ```bash
  npx eslint ee/server/src/components/workflow-designer/WorkflowDesignerPalette.tsx ee/server/src/components/workflow-designer/PaletteItemWithTooltip.tsx ee/server/src/components/workflow-designer/WorkflowDesigner.tsx
  npx vitest run src/components/workflow-designer/__tests__/WorkflowDesignerPalette.test.tsx
  node scripts/validate-translations.cjs
  ```
- Results:
  - ESLint reports 0 errors across the three files; warnings are pre-existing.
  - Vitest: 2/2 palette tests pass.
  - translation validation passed with 0 missing/extra keys.

### F026 complete — ActionSchemaReference + GroupedActionConfigSection localized
- `ActionSchemaReference.tsx`:
  - Both inner components (`SchemaFieldRow`, `SchemaReferenceSection`) and the public `ActionSchemaReference` now call `useTranslation('msp/workflows')`.
  - Localized: constraint tooltip lines (`Values`, `Min`, `Max`, `Min length`, `Max length`, `Pattern`, `Format`, `Examples`, `Default`) emitted as interpolated `{{value}}`/`{{list}}` strings, nullable suffix `| null`, per-field copy title `Copy {{path}}`, and the default "No fields" empty message.
  - Localized top-level section: select-an-action empty state, view/hide schema details toggle, input/output schema section titles, both section empty messages (`No input parameters`/`No output fields`), "Output available at …" success banner prefix, raw-JSON show/hide toggle, export-schema button + tooltip, and the `// Input Schema` / `// Output Schema` inline comments inside the raw schema viewer.
  - `SchemaReferenceSection` now accepts an optional `emptyMessage` and falls back to the localized `schemaReference.noFields` when not provided, so the component contract stays backward-compatible for future callers.
  - "Copy all paths" toolbar: title, `Copy all paths` label, `Copied!` success, and the `onCopyPath` toast string `{{count}} paths copied`.
- `GroupedActionConfigSection.tsx`:
  - Localized `Group` header, the inline action `CustomSelect` label + placeholder (`Select a {{group}} action`), and the action-required error card (`Action required` title + parameterized message).
  - Renamed the module-level `TILE_KIND_LABELS` to `TILE_KIND_LABEL_DEFAULTS` and now resolves the badge label through `t(\`groupedAction.tileKind.${record.tileKind}\`, …)` so translations can override `Core` / `Transform` / `App` / `AI` per locale.
- Added `schemaReference.*` (31 keys) and `groupedAction.*` (9 keys) to `server/public/locales/en/msp/workflows.json`, synced the same stub structure into `fr/es/de/nl/it/pl/xx/yy`.
- Checks run:
  ```bash
  npx eslint ee/server/src/components/workflow-designer/ActionSchemaReference.tsx ee/server/src/components/workflow-designer/GroupedActionConfigSection.tsx
  node scripts/validate-translations.cjs
  ```
- Results: ESLint clean (0 errors, 0 warnings); translation validation passed with 0 missing/extra keys.

### F027 complete — mapping components localized
- `ValidationBadge.tsx` — Status labels (`Valid`/`Warnings`/`Errors`/`Incomplete`) now resolve via `t(\`validationBadge.status.${status}\`)` with the original English as `defaultValue`. Also localized: tooltip copy (both `All required inputs are mapped` and `Configure input mappings`), the `{mapped} of {required} required fields mapped` interpolated line, both `Open Mapping Editor` CTAs, the `Errors (n)`/`Warnings (n)` expanded-section headings, and the `+N more errors`/`+N more warnings` truncation tail rows.
- `SourceDataTree.tsx` — Localized the search placeholder, all five section titles (`Payload`, `Step Outputs (vars)`, `Loop Context`, `Workflow Meta`, `Error Context`), the empty-vars helper copy (split into 5 ordered pieces to preserve the inline `vars.<name>` code span), and both loop-context badges (`current item`, `loop index`).
- `InputMappingEditor.tsx` — Five React components inside this 1.8k-line file now call `useTranslation('msp/workflows')`:
  - `MappingFieldEditor` — `Browse sources` toggle, `Use reference`/`Use fixed value` legacy-replacement buttons and their explanatory card (`Legacy mapping no longer supported here` + description).
  - `StructuredLiteralGroup` — `Collapse {{title}}` / `Expand {{title}}` aria-labels for the expand/collapse button.
  - `FixedValueEditorShell` — `Open editor` trigger, the dialog `Edit {{fieldName}}` title (used in both the Dialog component's `title` prop and the inner `DialogTitle`), `Cancel`/`Apply` footer buttons, and the dialog description `Use the larger editor for longer fixed-value content.`.
  - `LiteralValueEditor` — nullable select options (`Use value`/`Set null`), editor mode select options (`Structured`/`Raw JSON`), the `Invalid JSON` error toast/label, object-fields section title + `Reset`, per-row `Item {{index}}` titles and their `Reset` buttons (replaced by a single `replace_all` edit), `Add item` buttons, primitive-array placeholder (`Enter one value per line, or comma-separated`) + helper (`Use newline, comma, or semicolon separators.`), and the default string-input placeholder `Enter value...`.
  - Top-level `InputMappingEditor` — Empty state (`This action has no input fields.`), list-box / field-list ARIA labels, `{{filled}} of {{total}} fields filled` summary + `{{count}} required missing` + its red badge tooltip, `Apply suggestions ({{count}})` + `Clear values` bulk actions, `(fuzzy)` confidence suffix, `Apply suggestion: {{sourcePath}}` button tooltip, `Remove mapping (Delete/Backspace)` per-row trash button tooltip, `Fill` add-mapping button.
- `MappingPanel.tsx`, `MappingEditorSkeleton.tsx`, `MappingConnectionsOverlay.tsx` — no user-visible hardcoded English strings; all three components are purely presentational / data-pass-through. No changes required.
- Added `sourceDataTree.*` (14 keys), `validationBadge.*` (12 keys), and `inputMappingEditor.*` (~35 keys) to `server/public/locales/en/msp/workflows.json`, synced the same stub structure into `fr/es/de/nl/it/pl/xx/yy`.
- Deliberately deferred out of F027:
  - The inline array-validation error strings (`Item {index} must be an integer`, `At most {n} value(s)…`, etc.) produced by `parsePrimitiveList` — these are validator return values pushed up through `onChange` plumbing, not direct UI strings. Better handled with the validator module or as part of F040's server-error mapping pass so the same `errors[]` shape works across client and server validators.
  - The literal numeric `number` editor, boolean labels (`true`/`false`), and enum pass-through values — those are rendered verbatim from option values and don't need localization per PRD "node body text driven by workflow data remains unchanged".
  - `mapping/ExpressionTextArea.tsx` and `mapping/ExpressionAutocomplete.tsx` — scoped to F028.
- Checks run:
  ```bash
  npx eslint ee/server/src/components/workflow-designer/mapping/InputMappingEditor.tsx ee/server/src/components/workflow-designer/mapping/SourceDataTree.tsx ee/server/src/components/workflow-designer/mapping/ValidationBadge.tsx
  node scripts/validate-translations.cjs
  ```
- Results:
  - ESLint reports 0 errors across the three files; warnings are the pre-existing `no-non-null-assertion`, `no-unused-vars` backlog.
  - translation validation passed with 0 missing/extra keys.

### F028 complete — expression editor surfaces localized
- `ExpressionEditor.tsx` — translated the Monaco `ariaLabel` default to `expressionEditor.ariaLabel` (`'Expression editor'`). All other Monaco-internal UI (tooltips, error squiggles, command palette, keyboard shortcut list) stays vendor-rendered per PRD risk note "Vendor Monaco UI remains untranslated."
- `ExpressionEditorField.tsx` — translated the field-wrapper default placeholder (`'Enter expression...'`) and the inline field-picker `CustomSelect` placeholder (`'Insert field'`). The `placeholder` prop still accepts caller overrides; the `resolvedPlaceholder` inside the component falls back to the translated default when none is provided.
- `mapping/ExpressionAutocomplete.tsx` — translated the listbox `aria-label` (`'Expression autocomplete suggestions'`). Suggestion rows render path/type/description from context data, which is not chrome.
- `mapping/ExpressionTextArea.tsx` — translated the fallback placeholder `'Enter JSONata expression...'` via the same optional-prop + resolved-value pattern.
- Added `expressionEditor.*` (5 keys: `ariaLabel`, `autocompleteAria`, `textAreaPlaceholder`, `field.placeholder`, `field.insertFieldPlaceholder`) to `server/public/locales/en/msp/workflows.json`, synced the same stub structure into `fr/es/de/nl/it/pl/xx/yy`.
- Deliberately out of scope:
  - Monaco's built-in UI (suggestion widget, hover popover, problem markers) — PRD calls this out explicitly.
  - Inline data `description` strings in the seeded context schema (`Workflow state`, `Trace ID`, `Error name`, etc.) — these surface in Monaco tooltips and are seeded by this file for the UI-rendered schema; translating them would desync Monaco's schema store with other providers that share the same context. Revisit via the expression-context provider if/when those tooltips become reader-facing chrome rather than developer diagnostics.
- Checks run:
  ```bash
  npx eslint ee/server/src/components/workflow-designer/expression-editor/ExpressionEditor.tsx ee/server/src/components/workflow-designer/expression-editor/ExpressionEditorField.tsx ee/server/src/components/workflow-designer/mapping/ExpressionAutocomplete.tsx ee/server/src/components/workflow-designer/mapping/ExpressionTextArea.tsx
  node scripts/validate-translations.cjs
  ```
- Results: ESLint 0 errors (remaining warnings are the file's existing `no-unused-vars` backlog); translation validation passed with 0 missing/extra keys.

### F029 complete — WorkflowActionInput* files localized
- `WorkflowActionInputFieldInfo.tsx` — added `useTranslation('msp/workflows')` and threaded `t` into `buildConstraintHints` as a parameter so the module-level helper can emit localized constraint lines (`Format: …`, `Each item: …`, `Length: min - max`, `Range: min - max`, `any`/∞ fallbacks) while staying outside the React component. `Required` badge text, its hover-title in both states, `Default:` and `Example:` prefixes all translate via `actionInputFieldInfo.*`.
- `WorkflowActionInputSection.tsx` — converted the arrow-expression component body to a function body so it can call `useTranslation` and translate the `Action inputs` heading.
- `WorkflowActionInputSourceMode.tsx` / `WorkflowActionInputTypeHint.tsx` — both already read all copy from the shared enum hooks or prop-driven data; zero hardcoded strings, no changes required.
- `WorkflowActionInputFixedPicker.tsx` (843 lines):
  - Renamed `TICKET_PICKER_DEPENDENCY_HINTS` → `TICKET_PICKER_DEPENDENCY_HINT_DEFAULTS` so the hint text can be looked up by i18next key (`actionInputFixedPicker.dependencyHints.{kind}.{path}`) with the English as `defaultValue`. All 7 dependency-hint strings across 5 picker kinds now translate.
  - `buildDisabledExplanation` and `getWorkflowPickerPlaceholder` now accept a `TFunction` so they can stay module-level but emit localized output; both call sites inside the React component pass the local `t` from `useTranslation`.
  - `renderDedicatedPicker` (another module-level helper) also accepts `t`; all five fallback picker placeholders (`Select Board` / `Select Client` / `Select Contact` / `Select User` / `Select User or Team`) now translate, while caller-provided `fixedValueHint` overrides win as before.
  - `WorkflowTicketPicker` inner component — translates the ticket search placeholder, the two `CustomSelect` state-dependent placeholders (`Select ticket` vs `Type above to search tickets`), and both `setLoadError` fallbacks (`Failed to load ticket`, `Failed to search tickets`).
  - Main component — translates `Failed to load options` fallback and `Loading options...` placeholder via the parameterized helpers.
- Added `actionInputFieldInfo.*` (10 keys), `actionInputSection.*` (1 key), and `actionInputFixedPicker.*` (18 keys) to `server/public/locales/en/msp/workflows.json`, synced the same stub structure into `fr/es/de/nl/it/pl/xx/yy`.
- Checks run:
  ```bash
  npx eslint ee/server/src/components/workflow-designer/WorkflowActionInput*.tsx
  node scripts/validate-translations.cjs
  ```
- Results: ESLint clean (0 errors, 0 warnings); translation validation passed with 0 missing/extra keys.
- Checks run:
  ```bash
  npx eslint ee/server/src/components/workflow-graph/WorkflowGraph.tsx
  node scripts/validate-translations.cjs
  ```
- Results:
  - ESLint reports 0 errors; remaining warnings are pre-existing `any` / non-null-assertion / `explicit-function-return-type` sites, plus the already-present backlog.
  - translation validation passed with 0 missing/extra keys.

### F030–F034 complete — designer editor surfaces localized
Batched six related surfaces in one commit since each has relatively few chrome strings:
- **F030 `WorkflowAiSchemaSection.tsx`** — Both the public section and the inner `FieldEditor` now call `useTranslation`. Localized the mode-toggle buttons (`Simple`/`Advanced`), every field-row label (`Name`, `Answer type`, `Array items`, `Required`, `Description`), the nested-field area heading (`Object item fields`/`Nested fields`), the `Add field`/`Add nested field`/`Remove` buttons, the JSON-Schema label + advanced helper text, the fallback warning, the simple-mode hydration error, the `Schema validation` header, and the `AI output schema JSON is required.` parse-error fallback. `getHydrationError` and `deriveSectionState` now accept a `TFunction` so they can emit localized strings without becoming React components.
- **F031 `WorkflowComposeTextSection.tsx` + `WorkflowComposeTextDocumentEditor.tsx`** — Section heading + description, `Add output`, untitled fallback, the three aria-labels on move/delete buttons (parameterized with `{{label}}`), `Output label`/`Stable key` input labels, safe/invalid key hint text, `Regenerate`, the `Validation` card heading, `Downstream reference path` + `Save output to see a reference path.` fallback, `Copy path`/`Copied` toggle, `Compose content` heading + description, `Insert reference`/`Insert workflow reference` button + picker heading, and the `References cannot be inserted inside code blocks.` error toast. The BlockNote `placeholders.default` value and the block-type dropdown `name` strings (`Paragraph`, `Heading 1`, `Bullet List`, `Code Block`, etc.) stay English because they are rendered by the BlockNote vendor toolbar which doesn't accept i18n wiring — flagged with an inline comment for the vendor-UI backlog.
- **F032 `WorkflowStepNameField.tsx`** — Converted to function body; label `Step name` now translates via `stepNameField.label`.
- **F032 `WorkflowStepSaveOutputSection.tsx`** — `Save output` toggle label, the `e.g., ticketDefaults` placeholder, the `Copy full path` title, and the `Accessible as:` caption all translate. The auto-generated variable name `result` stays as a literal since it surfaces in user-authored workflows as `vars.result` — added an inline comment so the intent is clear.
- **F032 WorkflowWaitEditors** — file does not exist in the current tree; all wait-related UI (fixed time picker, duration editor, wait-mode toggle) lives inline inside `WorkflowDesigner.tsx::StepConfigPanel` and was partially covered by F012 (enum options). No separate component to localize.
- **F033 `workflowReferenceSelector.tsx`** — `ReferenceScopeSelector` inner component added `useTranslation`; the three `CustomSelect` placeholders (`Select source scope…`, `Select step…`, `Select field…`) now resolve via `referenceSelector.placeholders.*`.
- **F034 `pipeline/PipelineComponents.tsx`** — All three components (`PipelineStart`, `PipelineConnector`, `EmptyPipeline`) call `useTranslation`. Translated the `Start` indicator label, the `Insert step here` insert-button tooltip, and both empty-pipeline messages (`No steps yet.` when disabled, `Select a step from the panel to get started.` otherwise). The step-card summary and `BranchLabel` inner components render data-driven content and don't have chrome strings.
- Added new keys to `server/public/locales/en/msp/workflows.json` (6 new top-level blocks: `stepNameField`, `stepSaveOutput`, `aiSchemaSection`, `composeText`, `referenceSelector`, `pipeline` — 68 keys total), synced the same stub structure into `fr/es/de/nl/it/pl/xx/yy`.
- Checks run:
  ```bash
  npx eslint ee/server/src/components/workflow-designer/WorkflowAiSchemaSection.tsx ee/server/src/components/workflow-designer/WorkflowComposeText*.tsx ee/server/src/components/workflow-designer/WorkflowStepNameField.tsx ee/server/src/components/workflow-designer/WorkflowStepSaveOutputSection.tsx ee/server/src/components/workflow-designer/workflowReferenceSelector.tsx ee/server/src/components/workflow-designer/pipeline/PipelineComponents.tsx
  node scripts/validate-translations.cjs
  ```
- Results: ESLint 0 errors (17 warnings, all pre-existing `no-unused-vars`/`no-non-null-assertion` backlog); translation validation passed with 0 missing/extra keys.

### F035–F039 complete — task inbox components localized
- **F035 `TaskInbox.tsx`** — `Task Inbox` title, `← Back to Task List` button, and the four tabs (`Pending`, `Claimed`, `Completed`, `All Tasks`) all route through `taskInbox.*`.
- **F036 `TaskList.tsx`** — Added `useTranslation` to both the exported `TaskList` and the inner `Pagination` helper. Localized pagination controls (`Previous`, `Next`, `Page {{current}} of {{total}}`), load/claim/unclaim error fallbacks + the shared `Unknown error`, `Claim`/`Unclaim` row actions, the empty state (`No tasks found`), the `No due date` placeholder + `(Overdue)` suffix + `Due:` label. Switched the per-row due-date rendering to `useFormatters().formatDate(...)` so the month/day order follows the user's locale.
- **F036 `TaskDetails.tsx`** — Localized every field label (`Status`, `Priority`, `Created`, `Due Date`, `Claimed By`, `Completed By`), the tabs (`Details`, `Form`, `History`), the context/response-data panel headings, the `N/A` fallback, `Task not found`, the `Retry` error-recovery button, the claim/unclaim buttons (`Claim Task` / `Unclaim Task`), the `Claimed By`/`Completed By` `You` pronoun, and the `No form available for this task.` empty-form message. Error strings for load/claim/unclaim now interpolate the underlying `err.message` via `{{error}}`.
- **F036 `TaskHistory.tsx`** — Localized the `No history available for this task.` empty state, the `By:`/`System`/`Details` inline labels, the six `getActionLabel` cases (`Created`/`Claimed`/`Unclaimed`/`Completed`/`Canceled`/`Expired`), the load-failure toast, and the timestamp renderer via `useFormatters().formatDate`.
- **F037 `TaskForm.tsx`** — `Complete Task` and `Cancel` default-action labels now translate. Other task-action labels remain caller-provided.
- **F037 `DynamicForm.tsx`** — Default `Submit` and `Cancel` action labels, plus the generic `An error occurred` handler-fallback, now translate. RJSF vendor-generated field labels/error messages stay schema-driven per PRD.
- **F038 `EmbeddedTaskInbox.tsx`** — `My Tasks` header, `View All` link, and `← Back to Tasks` button now translate.
- **F039 `ActionButton.tsx`** — `Processing...` button state, `Confirm Action` dialog title, and the dialog `Cancel`/`Confirm` footer buttons all translate. `ActionButtonGroup.tsx` has no hardcoded strings — it renders caller-provided action labels.
- Added new keys to `server/public/locales/en/msp/workflows.json` (8 new top-level blocks: `taskInbox`, `taskList`, `taskDetails`, `taskHistory`, `taskForm`, `dynamicForm`, `embeddedTaskInbox`, `actionButton` — ~60 keys total), synced the same stub structure into `fr/es/de/nl/it/pl/xx/yy`.
- Checks run:
  ```bash
  npx eslint ee/packages/workflows/src/components/workflow/TaskInbox.tsx ee/packages/workflows/src/components/workflow/TaskList.tsx ee/packages/workflows/src/components/workflow/TaskDetails.tsx ee/packages/workflows/src/components/workflow/TaskHistory.tsx ee/packages/workflows/src/components/workflow/TaskForm.tsx ee/packages/workflows/src/components/workflow/DynamicForm.tsx ee/packages/workflows/src/components/workflow/EmbeddedTaskInbox.tsx ee/packages/workflows/src/components/workflow/ActionButton.tsx
  node scripts/validate-translations.cjs
  ```
- Results: ESLint 0 errors (remaining warnings are pre-existing `no-unused-vars`, `exhaustive-deps`, and unused-prop backlog); translation validation passed with 0 missing/extra keys.

### F041 complete — pseudo-locales regenerated
- Ran `node scripts/generate-pseudo-locales.cjs`: `Generated 62 pseudo-locale files from 31 English sources.`
- `server/public/locales/xx/msp/workflows.json` (underscored pseudo) and `server/public/locales/yy/msp/workflows.json` (11111-pattern pseudo) now reflect the full extended namespace with all keys added across F001–F039.
- Validation: `node scripts/validate-translations.cjs` passes with 0 missing / 0 extra across 8 locales.

### F042–F045 complete — locales populated with context-aware translations
- Added `scripts/translate-workflows-locales.cjs` — a per-language translation dictionary keyed by the English source strings. It recursively walks `server/public/locales/en/msp/workflows.json` and emits a translated file for each of `fr/es/de/nl/it/pl`. Strings not covered by a language's dictionary keep the English value (valid fallback via the `defaultValue` pattern that every `t()` call uses).
- Per-locale override counts (unique source strings):
  - **fr**: 385 overrides — full coverage of toolbar, dialogs, trigger/schedule labels, task inbox, designer chrome, mapping editor, schema reference, expression editor, compose text, pipeline + graph chrome, run studio, and all error/toast fallbacks.
  - **es**: 385 overrides (same surface).
  - **de**: 385 overrides (same surface, formal "Sie" register).
  - **nl**: 385 overrides (starts from the German dictionary then applies Dutch-specific overrides).
  - **it**: 118 overrides — high-frequency chrome (actions, statuses, dialogs, common task labels).
  - **pl**: 118 overrides — same high-frequency chrome; Polish plural suffixes not needed yet because the base keys don't use plural syntax.
  - **Total**: ~1,776 unique translated strings across the six locales.
- Variables (`{{version}}`, `{{count}}`, `{{fieldName}}`, etc.) are preserved verbatim in every translated value.
- Acronyms stay English per the style guide: `CSV`, `JSON`, `API`, `URL`, `UUID`, `ID`, `SLA`, `UI`.
- Formal register consistent with `packages/billing` / `msp/clients` translations: French "vous", Spanish "usted", German "Sie", Dutch "u", Italian "Lei", Polish formal 2nd person.
- **F043** — Italian accent audit: `grep -n ' e [a-z]\| puo \| gia \| verra \| funzionalita\| necessario' server/public/locales/it/msp/workflows.json` returns zero matches. Accented forms (è, à, ù) are used correctly where needed (e.g., `Priorità`, `Attività`).
- **F044** — The translated tab/section names align with `msp/core.json` per language (Workflows / Designer / Runs / Tasks / Schedules). The `Dead Letter` label translates to `Lettre morte` (fr) / `Carta muerta` (es) / `Unzustellbar` (de); these are new to this namespace and not duplicated from `msp/core.json`.
- **F045** — `node scripts/validate-translations.cjs` passes with 0 missing / 0 extra across all 8 non-English locales.
- Remaining translation coverage gap: the deeper descriptive strings in `runList.*`, `runDetails.*`, `schedules.*`, `designer.toasts.*`, etc. are not yet in the per-locale dictionaries for it/pl. Those surfaces still render English fallbacks via `defaultValue`. Adding Italian/Polish entries for those blocks is a straightforward follow-up — extend `translations.it` / `translations.pl` in `scripts/translate-workflows-locales.cjs` with the remaining English source strings and re-run the script. The rerun is safe (idempotent) because any source string without an override is preserved as English.

### F040 complete — server-action errors mapped to localized toasts
- Added `ee/server/src/components/workflow-designer/workflowServerErrors.ts` — a `mapWorkflowServerError(t, err, fallback)` helper plus a `KNOWN_ERROR_KEYS` dictionary. The dictionary matches ~40 distinct English server-action error strings (permission/authn, workflow lookup, publish/delete blockers, run start/retry/cancel/resume/replay, schedule validation, cron validation, event processing) to `serverErrors.*` translation keys. A versioned error (`Workflow version {{version}} already exists…`) is handled by a small regex that extracts the version number and passes it to the translator.
- Added `serverErrors.*` (~40 keys) to `server/public/locales/en/msp/workflows.json`, with proper translations for `fr/es/de/nl/it/pl` via `scripts/translate-workflows-locales.cjs`.
- Wired the helper into every remaining `toast.error(error instanceof Error ? error.message : …)` site in the workflow flows:
  - `WorkflowDesigner.tsx` (4 sites: load workflows, save, settings update, publish)
  - `WorkflowRunDialog.tsx` (2 sites: latest-run load, start run)
  - `WorkflowRunDetails.tsx` (11 sites: load details, logs, audit, retry, cancel, resume, replay, export, etc. — swapped via a small parser script)
  - `WorkflowDeadLetterQueue.tsx`, `WorkflowEventList.tsx`, `WorkflowDefinitionAudit.tsx` — all remaining dual-mode toasts
  - `RunStudioShell.tsx` (replay/cancel action failure)
- Fallback behavior: unmapped server errors still render their raw English `err.message` to the user so nothing is silently dropped, but any matching server error now renders the localized string in fr/es/de/nl. `it`/`pl` cover the full server-error set as well (they're part of the 40-key block I translated, even though their general dictionaries are still lighter).
- Ran `scripts/translate-workflows-locales.cjs` + `scripts/generate-pseudo-locales.cjs` + `scripts/validate-translations.cjs` — all green (0 missing, 0 extra across 8 locales).

### F047–F050 complete — docs updates
- **F047** (`MSP_i18n_plan.md`) — Executive Summary bumped to 33 namespace files / ~13,346+ keys / ~545 components; `msp/workflows` row flipped to ✅ DONE with final 1,125-key count and short summary of what shipped; in-progress list no longer mentions workflows.
- **F048** (`translation_files_structure.md`) — Added `msp/workflows.json` to the directory listing (1,611 lines, 1,125 keys), the namespace table (summary + component count), and the four route mappings (`/msp/workflows`, `/msp/workflows/runs`, `/msp/workflow-editor`, `/msp/workflow-control`). Updated the overall "keys per language" line to 11,084.
- **F049** (`MSP_i18n_plan.md`) — Removed stale "workflows in progress" lines from Phase 7 remaining-work list; replaced Section 8.5 (old "scope growth" list) with a "Batch 2b-9 shipped" summary that lists the 48 wired components, the 13 enums, the colocated trigger presentation hooks, the server-error map, and the remaining `it`/`pl`/`pt` translation follow-ups. Also updated the "remaining work" queue item #9 to reflect the follow-ups rather than the multi-day batch.
- **F050** (`enum-labels-pattern.md`) — Added a `✅ Batch 11 — Workflows (shipped 2026-04-20)` entry under "Non-billing backlog" listing all 13 enums, the consolidated constants + hooks files, the seven component sites where inline option arrays were eliminated, and the trigger/schedule helper move. Flagged `getActivityStatusOptions()` as remaining backlog pending a concrete UI surface.
