# Feature Spec: English and Spanish App Localization

**Status:** Draft  
**Scope:** Signed-in app UI, user preference, emails, AI-generated text, knowledge base display and creation, ticket responses, tests  
**Languages:** English (`en`) and Spanish (`es`) only

---

## Problem

Ask QIEN currently behaves as an English-only application. For Spanish-speaking line managers and experts, that creates friction at every important point: ticket submission, clarification questions, expert responses, email notifications, and knowledge base reuse.

The goal is not to build a general internationalization platform. The product should support exactly two languages: English and Spanish. A signed-in user should be able to choose their preferred language, and the app should present UI, system-generated content, KB content, and responses in that language wherever feasible.

---

## Product Rules

1. **Supported languages are closed:** `en` and `es`. No locale variants, no future-language abstraction work beyond what keeps this pair maintainable.
2. **Preference is per user:** store language on `users`, not tenant config. Mixed-language tenants are expected.
3. **Invite sets the initial language:** admins/experts choose English or Spanish when inviting a user. The invited user can change it during onboarding or later in settings.
4. **User-authored text is preserved:** original ticket messages, expert responses, and KB edits remain auditable in the language they were written.
5. **Generated text follows the target reader:** clarification questions and response emails use the line manager's language; expert summaries/drafts use the expert's language; KB pages use the viewer's language.
6. **No route prefixes:** do not introduce `/es/...` routes. Language is derived from the signed-in user preference.
7. **Translations must not drift:** when editable durable content changes in one language, the paired translation must be regenerated or marked stale before it is shown as current.
8. **Marketing/public pages are out of scope for the first signed-in app pass** unless we explicitly decide otherwise.

---

## Design Intent

Use a simple first-party localization layer instead of a large generic i18n framework unless implementation proves otherwise. The current app is mostly App Router server components plus focused client components, so a typed dictionary module and small server/client helpers should be enough.

For user-facing durable content, store both English and Spanish variants instead of translating on every page load. "Durable content" means content that users rely on later: ticket responses, clarification questions, KB entries, KB revisions, summaries shown in the workflow, and notification bodies that include generated guidance.

Translation-on-demand is only acceptable as a write operation, not a render strategy: generate the missing variant, persist it with source metadata, and either show it for expert review or mark it as machine generated. Do not repeatedly translate the same KB, response, summary, or notification content during page render. The UI can still localize static labels through dictionaries without storing duplicate labels in the database.

---

## Data Model

### Users

Add:

```sql
users.preferred_language TEXT NOT NULL DEFAULT 'en'
```

Application type:

```ts
export const SUPPORTED_LANGUAGES = ['en', 'es'] as const
export type SupportedLanguage = (typeof SUPPORTED_LANGUAGES)[number]
```

Validation must reject any value other than `en` or `es`.

When an admin/expert invites a new user, the selected invite language seeds `users.preferred_language`. For existing users being added to another tenant or reactivated, do not overwrite their existing preference unless the admin explicitly changes it in a future user-management enhancement.

### Dynamic Content Language Metadata

Add `language` where user-visible content is created or persisted:

| Table | Field | Purpose |
|---|---|---|
| `ticket_messages` | `language` | language of the stored message body |
| `ai_artifacts` | `language` | language of generated summary, draft, sentiment, or completeness output |
| `knowledge_entries` | `language` | canonical language of the current entry body |
| `knowledge_entry_revisions` | `language` | language of each revision |

Default existing rows to `en`.

For content that has a paired translation, store a source pointer so stale translations can be detected:

| Content | Required drift control |
|---|---|
| KB translation | `source_revision` must match the current canonical revision |
| Response translation | source message ID plus source text hash or version |
| AI artifact translation | source artifact ID plus source text hash or version |

If the source text changes and the stored translation no longer matches the source pointer, do not display it as current. Regenerate it or show a stale/missing translation state.

### KB Translation Storage

Add a new table for durable translated KB variants:

```sql
knowledge_entry_translations
  id              UUID PK DEFAULT gen_random_uuid()
  tenant_id       UUID NOT NULL FK -> tenants.id
  entry_id        UUID NOT NULL FK -> knowledge_entries.id ON DELETE CASCADE
  language        TEXT NOT NULL CHECK (language IN ('en', 'es'))
  title           TEXT NOT NULL
  body_markdown   TEXT NOT NULL
  flat_markdown   TEXT NOT NULL
  source_revision INT NOT NULL
  generated_by    TEXT NOT NULL DEFAULT 'llm'
  status          TEXT NOT NULL DEFAULT 'machine_generated'
  reviewed_by_id  UUID NULL FK -> users.id
  reviewed_at     TIMESTAMPTZ NULL
  created_at      TIMESTAMPTZ NOT NULL DEFAULT now()
  updated_at      TIMESTAMPTZ NOT NULL DEFAULT now()

UNIQUE (entry_id, language)
```

Do not create translation rows for the entry's own canonical language.

`status` should be one of `machine_generated`, `reviewed`, or `stale`. A stale translation must not be used as the normal viewer-language KB body.

### Response Translation Storage

Add durable storage for translated sent responses and other generated ticket content where both languages may be needed:

```sql
ticket_message_translations
  id                UUID PK DEFAULT gen_random_uuid()
  tenant_id         UUID NOT NULL FK -> tenants.id
  message_id        UUID NOT NULL FK -> ticket_messages.id ON DELETE CASCADE
  language          TEXT NOT NULL CHECK (language IN ('en', 'es'))
  body              TEXT NOT NULL
  source_body_hash  TEXT NOT NULL
  status            TEXT NOT NULL DEFAULT 'machine_generated'
  reviewed_by_id    UUID NULL FK -> users.id
  reviewed_at       TIMESTAMPTZ NULL
  created_at        TIMESTAMPTZ NOT NULL DEFAULT now()
  updated_at        TIMESTAMPTZ NOT NULL DEFAULT now()

UNIQUE (message_id, language)
```

Do not create a translation row for the message's own language. If the source message body is edited, recompute `source_body_hash`; any existing translation with the old hash becomes stale and must be regenerated before being sent or displayed as current.

### AI Artifact Translation Storage

For durable AI artifacts that users may see again, store translated variants instead of translating during render:

```sql
ai_artifact_translations
  id                UUID PK DEFAULT gen_random_uuid()
  tenant_id         UUID NOT NULL FK -> tenants.id
  artifact_id       UUID NOT NULL FK -> ai_artifacts.id ON DELETE CASCADE
  language          TEXT NOT NULL CHECK (language IN ('en', 'es'))
  content           TEXT NOT NULL
  content_json      TEXT NULL
  source_body_hash  TEXT NOT NULL
  status            TEXT NOT NULL DEFAULT 'machine_generated'
  reviewed_by_id    UUID NULL FK -> users.id
  reviewed_at       TIMESTAMPTZ NULL
  created_at        TIMESTAMPTZ NOT NULL DEFAULT now()
  updated_at        TIMESTAMPTZ NOT NULL DEFAULT now()

UNIQUE (artifact_id, language)
```

This applies to workflow-visible summaries, response drafts, completeness questions, and any generated guidance reused after the initial request.

---

## UI Localization

### Dictionary Shape

Create:

```text
src/lib/i18n/
  languages.ts
  dictionaries/en.ts
  dictionaries/es.ts
  server.ts
  client.tsx
```

Use stable keys, not inline English strings:

```ts
t('nav.submit')
t('ticket.status.responseReady')
t('settings.language.label')
```

Use typed dictionaries so missing Spanish keys fail type-check.

### Server Components

Server pages call a helper that reads `auth()`, loads the user's preferred language, and returns `t`.

```ts
const { language, t } = await getI18n()
```

Where a page already fetches the user, include `preferredLanguage` in the same query to avoid extra DB round trips.

### Client Components

Client components should receive translated labels as props for narrow components. For larger client surfaces such as `SubmitForm`, `SettingsClient`, `UsersClient`, and ticket action forms, wrap the signed-in layout in a lightweight `I18nProvider`.

### Form Options and Status Labels

Centralize labels for:

- Ticket statuses
- Roles
- Priorities
- Action requested values
- Issue categories
- Project active/inactive labels
- AI artifact labels
- Admin metric labels

Values stored in the database remain stable English-like enum values. Only labels change.

---

## Language Preference UX

### Onboarding

Add language selection to `/onboarding`:

- Label: `Preferred language` / `Idioma preferido`
- Options: `English`, `Español`
- Default: English
- Saving onboarding updates `displayName`, `timeZone`, and `preferredLanguage`

### Settings

Add the same selector to `/settings`.

Changing the preference should:

- Save immediately with the rest of the settings form
- Refresh the current route
- Re-render nav, labels, and future generated content in the new language

### Admin User Management

Admins/experts choose the invitee's initial language when inviting a user from `/admin/users`.

Invite form changes:

- Add `Language` selector with `English` and `Español`
- Default to English
- POST `{ email, role, preferredLanguage }`
- Validate `preferredLanguage` as `en | es`
- Store it on newly created users
- For reactivated users or existing users added to a tenant, preserve the existing user preference by default
- Send the invitation email in the selected language for newly invited users

The user can change the preference during onboarding or in settings.

---

## Emails

All signed-in-workflow emails should use the target recipient's preferred language:

| Email | Recipient language source |
|---|---|
| Magic link | `users.preferredLanguage` when the email exists; otherwise English fallback |
| Admin invite | invite form language selection |
| Ticket queued | submitter `preferredLanguage` |
| Clarification questions | submitter `preferredLanguage` |
| Expert response notification | submitter `preferredLanguage` |
| Follow-up received | expert recipient `preferredLanguage` |
| Expert digest | expert recipient `preferredLanguage` |

Email builders should take `language: SupportedLanguage` and use the dictionary layer for subject, preheader, boilerplate, and CTA text.

If a generated body is included in the email, generate or select that body in the recipient's language before sending.

---

## AI and Prompt Behavior

### Prompt Inputs

Every prompt builder should accept a `language` field:

- `prompts/completeness-assessment.ts`
- `prompts/summarization.ts`
- `prompts/response-draft.ts`
- `prompts/kb-update-proposal.ts`
- `prompts/sentiment-assessment.ts`
- `prompts/tag-suggestion.ts`

Use explicit wording:

```text
Write all user-visible output in Spanish.
Preserve product names, equipment IDs, SOP names, lot numbers, and quoted user text exactly.
```

For English:

```text
Write all user-visible output in English.
```

### Agent Language Selection

| Agent | Language |
|---|---|
| Completeness assessment questions | ticket submitter language |
| Summarization | generate and store the canonical artifact, then generate and store the paired language before showing it to a user who needs the other language |
| Response draft | expert user's language when draft is requested |
| KB update proposal | expert user's language, with canonical KB language rules below |
| Sentiment | language does not matter for numeric score; explanation follows expert/admin language when displayed |
| Tag suggestion | tags should remain normalized short labels; display labels can be localized separately |

### Response Language

An expert response is stored in the language the expert sends. If the line manager's preferred language differs, the app should provide a reviewed translation path before sending:

1. Expert drafts/responds in their preferred language.
2. If submitter language differs, generate a translated version for the submitter.
3. Expert sees both original and translated response.
4. Expert confirms before sending.
5. Store the original message and the translated variant with source hash/version metadata.

Do not silently send an AI translation that the expert did not see.

If the expert edits either the original or translated response before sending, regenerate the paired language or require the expert to update it. The sent English and Spanish variants should represent the same final guidance and must not drift.

---

## Knowledge Base Localization

### Canonical Entry

Each KB entry has one canonical language, set from the creator's preferred language unless explicitly changed by an expert/admin.

### Viewing

When a user views or searches the KB:

1. If the entry canonical language matches the viewer, show the entry.
2. Else if a reviewed translation exists, show it.
3. Else if an unreviewed translation exists, show it with a small expert-only "Machine translated" indicator.
4. Else show the canonical entry with a "Translate" action for experts/admins.

Line managers do not browse KB today; if that changes, do not show unreviewed machine translations to line managers.

### Search

For phase 1, keep embeddings based on canonical `flatMarkdown`. For Spanish queries, translate the search query to English when searching an English canonical KB, then display matching entries in the user's language when available.

For phase 2, add per-translation embeddings if Spanish KB usage is high enough to justify it.

### KB Update Proposals

When an expert sends a response:

- Generate the proposal in the expert's language.
- If accepted into a KB whose canonical language differs, generate a canonical-language version and show it for expert approval before saving.
- Create or update `knowledge_entry_translations` for the other language after acceptance.

### Edit Synchronization

When an expert/admin edits KB content in either language:

1. Save the edited version as the new source for that language.
2. Regenerate the paired language from the edited version.
3. Mark the regenerated translation as `machine_generated` until reviewed.
4. Store source revision/hash metadata linking the translation to the edited source.
5. Do not show an older translation as current once the source has changed.

The same rule applies to edited response content before send: the final sent English and Spanish bodies must be generated from the same edited source text and stored together.

### Translation Edit Override

Editing a translated KB variant does not automatically replace the canonical source language. This matters for an edge case where, for example, an English canonical KB entry has a machine-generated Spanish translation and a Spanish-speaking expert edits that Spanish text.

Default behavior:

1. Save the Spanish edit as a reviewed Spanish translation.
2. Link it to the current canonical English revision/hash.
3. Do not overwrite the English canonical entry.

The editor should also have an explicit promotion option:

- `Save as reviewed Spanish translation`
- `Also update English source`

If the expert chooses `Also update English source`, the system should generate an English back-translation from the edited Spanish text, show it to the expert/admin for approval, and only then save it as the new English canonical revision. After approval, regenerate or mark stale the paired Spanish translation according to the normal drift-control rules.

This avoids translation ping-pong while still allowing Spanish-native experts to correct operational meaning when the translated text is better than the original source.

---

## Recommended Phase Order

| Phase | Scope | Unlocks |
|---|---|---|
| 1 | Data model, invite language, and preference UI | Users can be invited into English/Spanish and can change language later |
| 2 | Static app UI dictionaries | Signed-in app chrome and forms render in preferred language |
| 3 | Email localization | Notifications match recipient language |
| 4 | AI output language controls | Clarifications, drafts, and generated text follow target language |
| 5 | KB translation workflow | KB can be viewed and maintained in both languages |
| 6 | Cross-language response review | Experts approve translated responses before submitters receive them |
| 7 | E2E hardening and rollout | Confidence across mixed-language tenants |

Each phase should be independently deployable.

---

## Punch List

### Phase 1 — Data Model and Preference UI

- [x] Add `preferredLanguage` to `users` with default `en`
- [x] Add language columns to `ticket_messages`, `ai_artifacts`, `knowledge_entries`, and `knowledge_entry_revisions`
- [x] Add `SupportedLanguage` helpers and validation
- [x] Add invite language selector to `/admin/users`
- [x] Update `/api/admin/users` to accept and validate `preferredLanguage`
- [x] Seed new invited users with the selected language
- [x] Update `/api/users/me/name` to accept and persist `preferredLanguage`
- [x] Add language selector to `/onboarding`
- [x] Add language selector to `/settings`
- [x] Include preferred language in session or a shared user preference loader
- [x] Add schema and API tests for allowed/rejected language values
- [x] Add admin invite tests for English and Spanish invite language
- [x] Backfill existing rows to `en`

### Phase 2 — Static App UI

- [x] Create typed English and Spanish dictionaries
- [x] Add server `getI18n()` helper
- [x] Add client `I18nProvider` for larger client components
- [x] Localize nav, onboarding, settings, submit, tickets, inbox, KB, admin users, admin config, platform health, and auth-adjacent signed-in screens
- [x] Centralize localized labels for statuses, roles, priorities, categories, and action requested values
- [x] Add tests that English and Spanish dictionaries have identical keys
- [x] Add component/page tests for at least settings, submit, inbox, and ticket detail in Spanish

### Phase 3 — Emails

- [x] Update email builders to accept `language`
- [x] Localize subject lines, CTA text, footer text, and static body copy
- [x] Fetch recipient language before sending workflow emails
- [x] Send admin invite emails in the invite form language
- [x] Send magic links in `users.preferredLanguage` when a user row exists
- [x] Add email snapshot/string tests in English and Spanish

### Phase 4 — AI Output Language

- [x] Add `language` input to every prompt builder
- [x] Update prompt tests for English and Spanish instructions
- [x] Fetch submitter language for completeness clarification questions
- [x] Fetch expert language for response draft requests
- [x] Store generated artifact language
- [x] Add mock-LLM integration tests proving language is passed through for completeness and response draft agents

### Phase 5 — Knowledge Base

- [x] Add `knowledge_entry_translations`
- [x] Add `ticket_message_translations` or equivalent durable translation storage for sent responses
- [x] Add `ai_artifact_translations` or equivalent durable translation storage for workflow-visible AI artifacts
- [x] Add normalization helpers for canonical vs translated KB content
- [x] Add source revision/hash drift detection for translated KB and response content
- [x] Update KB detail page to select the viewer-language variant
- [x] Update KB search result display to show viewer-language titles/snippets where available
- [x] Add expert/admin translation generation action
- [x] Add expert review state for machine translations
- [ ] Add translated-KB edit override: save as reviewed translation by default, with explicit promotion to update canonical source
  - Current state: edited Spanish translations can be saved as reviewed translations without changing canonical English. Explicit promotion/back-translation into the canonical source is still not implemented.
- [x] Add integration tests for viewing Spanish translations and falling back safely

### Phase 6 — Cross-Language Responses

- [ ] Detect when expert language differs from submitter language
  - Current state: server preview detects language differences, but the client response form only prompts translation review for non-English submitters. Spanish expert -> English submitter still needs UI coverage.
- [ ] Generate submitter-language response translation
  - Current state: preview API can generate the translation, but the client only exposes the flow for Spanish submitters.
- [x] Show original and translated response to expert before send
- [x] Store original and translated response bodies with source hash/version metadata
- [x] Regenerate or require update when either response variant is edited before send
- [x] Include the submitter-language body in response notification email
- [x] Add integration tests for English expert -> Spanish submitter and Spanish expert -> English submitter

### Phase 7 — E2E and Rollout

- [ ] Add e2e test for Spanish onboarding/settings preference
  - Current state: settings/profile language preference is covered; onboarding is not.
- [ ] Add e2e test for Spanish line manager submitting a ticket
  - Current state: Spanish submit page rendering is covered; full submission is not.
- [x] Add e2e test for expert sending response to Spanish line manager
- [x] Seed at least one Spanish user in test fixtures
- [ ] Document rollout and support expectations for bilingual tenants
- [ ] Run full suite twice before marking complete
  - Current state: latest run passed once with `pnpm type-check && pnpm test`.

---

## Done Criteria

- A signed-in user can choose English or Spanish in onboarding/settings.
- An admin/expert can choose English or Spanish when inviting a user.
- Signed-in app UI renders in the user's selected language.
- Workflow emails use the recipient's selected language once the recipient has a saved preference.
- AI-generated clarification questions and response drafts include explicit language instructions.
- KB entries can be displayed in the viewer's language when a translation exists.
- Edited KB translations can be saved as reviewed translations without automatically changing canonical source content.
- Experts approve cross-language response translations before they are sent.
- Edited KB and response content regenerates or invalidates the paired translation so English and Spanish do not drift.
- Existing English behavior remains unchanged for users with default `en`.
- `pnpm type-check && pnpm test` passes.
- Relevant e2e tests pass for both English and Spanish flows.

---

## Risks

### Regulated-content translation risk

Spanish translations of operational guidance can change meaning. This is highest risk for expert responses and KB content. Mitigation: expert review before sending translated responses; mark unreviewed KB translations; preserve original text and language metadata; invalidate stale translations after edits.

### Mixed-language tenant complexity

An English-speaking expert may support Spanish-speaking submitters, or vice versa. The system must avoid assuming one tenant language. Mitigation: use per-user preference and target-recipient language rules.

### Auditability

If translated content replaces original content, investigations become muddy. Mitigation: never overwrite original user-authored text; store language and translated variants separately.

### AI prompt drift

Adding language instructions can degrade structured JSON outputs if prompts become too broad. Mitigation: keep language instructions short and add mock-LLM tests around schema shape and language field propagation.

### KB search quality

English-only embeddings may underperform for Spanish queries. Mitigation: phase 1 translates Spanish queries before search; phase 2 can add per-language embeddings if needed.

### UI string coverage

Hardcoded strings are spread across server components, client components, API errors, emails, and tests. Mitigation: dictionary key parity tests and targeted Spanish render tests for core workflows.

### First-contact emails

Admin invite emails happen before onboarding, so they rely on the inviter's selected language. Magic links use the saved user preference when the user exists, with English fallback for unknown users. Risk remains if an admin selects the wrong invite language. Mitigation: make the invite language visible in the user-management table and let users change it during onboarding.

### Translation Drift

English and Spanish KB/response variants can diverge if one side is edited and the other is not regenerated. Mitigation: store source revision/hash metadata, mark paired translations stale on edit, and block sending or normal display when a required paired translation is stale.

### Test churn

Text assertions will break as UI strings move into dictionaries. Mitigation: update tests intentionally to assert semantic UI behavior, not incidental English copy, except where copy is the behavior under test.
