diff --git a/.planning/phases/07-multilanguage/07-RESEARCH.md b/.planning/phases/07-multilanguage/07-RESEARCH.md
new file mode 100644
index 0000000..1891871
--- /dev/null
+++ b/.planning/phases/07-multilanguage/07-RESEARCH.md
@@ -0,0 +1,633 @@
+# Phase 7: Multilanguage - Research
+
+**Researched:** 2026-03-25
+**Domain:** i18n (Next.js portal + AI agent language detection)
+**Confidence:** HIGH
+
+
+## User Constraints (from CONTEXT.md)
+
+### Locked Decisions
+- Default language: auto-detect from browser locale (e.g., es-MX → Spanish, pt-BR → Portuguese)
+- Switcher location: sidebar bottom, near user avatar — always accessible
+- Language preference: saved to portal_users table in DB — follows user across devices
+- Login page: has its own language switcher (uses browser locale before auth, cookie for pre-auth persistence)
+- Supported languages for v1: en (English), es (Spanish), pt (Portuguese)
+- Auto-detect from each incoming message — agent responds in the same language the user writes in
+- Applies across ALL channels: Slack, WhatsApp, Web Chat — consistent behavior everywhere
+- Fluid switching: agent follows each individual message's language, not locked to first message
+- Implementation: system prompt instruction to detect and mirror the user's language
+- No per-agent language config in v1 — auto-detect is the only mode
+- Portal UI: All pages, labels, buttons, navigation, placeholders, tooltips — fully translated
+- Agent templates: Names, descriptions, and personas translated in all 3 languages (DB seed data includes translations)
+- Wizard steps: All 5 wizard steps and review page fully translated
+- Onboarding flow: All 3 onboarding steps translated
+- Error messages: Validation text and error messages localized on the frontend
+- Invitation emails: Sent in the language the inviting admin is currently using
+- System notifications: Localized to match user's language preference
+
+### Claude's Discretion
+- i18n library choice (next-intl, next-i18next, or built-in Next.js i18n)
+- Translation file format (JSON, TypeScript, etc.)
+- Backend error message strategy (frontend-only translation recommended — cleaner separation)
+- How template translations are stored in DB (JSON column vs separate translations table)
+- RTL support (not needed for en/es/pt but architecture should not block it)
+- Date/number formatting per locale
+
+### Deferred Ideas (OUT OF SCOPE)
+None — discussion stayed within phase scope
+
+
+
+## Phase Requirements
+
+| ID | Description | Research Support |
+|----|-------------|-----------------|
+| I18N-01 | Portal UI fully localized in English, Spanish, and Portuguese (all pages, labels, buttons, error messages) | next-intl v4 with JSON message files; `useTranslations` hook in all components |
+| I18N-02 | Language switcher accessible from anywhere in the portal — selection persists across sessions | LanguageSwitcher component in Nav sidebar + login page; DB-backed preference via `/api/portal/users/me/language` PATCH; cookie for pre-auth state |
+| I18N-03 | AI Employees detect user language and respond accordingly | System prompt language instruction appended in `build_system_prompt()` and its TS mirror |
+| I18N-04 | Agent templates, wizard steps, and onboarding flow are fully translated in all three languages | JSON columns (`translations`) on `agent_templates` table; migration 009; all wizard/onboarding TSX strings extracted |
+| I18N-05 | Error messages, validation text, and system notifications are localized | All Zod messages and component-level error strings extracted to message files; backend returns error codes, frontend translates |
+| I18N-06 | Adding a new language requires only translation files, not code changes (extensible i18n architecture) | `SUPPORTED_LOCALES` constant + `messages/{locale}.json` file = complete new language addition |
+
+
+---
+
+## Summary
+
+Phase 7 adds full i18n to the Konstruct portal (Next.js 16 App Router) and language-aware behavior to AI Employees. The portal work is the largest surface area: every hardcoded English string in every TSX component needs extraction to translation JSON files. The AI Employee work is small: a single sentence appended to `build_system_prompt()`.
+
+The recommended approach is **next-intl v4 without URL-based locale routing**. The portal's current URL structure (`/dashboard`, `/agents`, etc.) stays unchanged — locale is stored in a cookie (pre-auth) and in `portal_users.language` (post-auth). next-intl reads the correct source per request context. This is the cleanest fit because (a) the CONTEXT.md decision requires DB persistence, (b) URL-prefixed routing would require restructuring all 10+ route segments, and (c) next-intl's "without i18n routing" setup is officially documented and production-ready.
+
+Agent template translations belong in a `translations` JSONB column on `agent_templates`. This avoids a separate join table, keeps migration simple (migration 009), and supports the extensibility requirement — adding Portuguese just adds a key to each template's JSON object.
+
+**Primary recommendation:** Use next-intl v4 (without i18n routing) + JSON message files + JSONB template translations column + system prompt language instruction.
+
+---
+
+## Standard Stack
+
+### Core
+| Library | Version | Purpose | Why Standard |
+|---------|---------|---------|--------------|
+| next-intl | ^4.8.3 | i18n for Next.js (translations, locale detection, formatting) | Official Next.js docs recommendation; native Server Component support; proxy.ts awareness built-in; 4x smaller than next-i18next |
+| next-intl/plugin | ^4.8.3 | next.config.ts integration | Required to enable `useTranslations` in Server Components |
+
+### Supporting
+| Library | Version | Purpose | When to Use |
+|---------|---------|---------|-------------|
+| @formatjs/intl-localematcher | ^0.5.x | RFC-compliant Accept-Language matching | Proxy-level browser locale detection before auth; already in Next.js i18n docs examples |
+| negotiator | ^0.6.x | HTTP Accept-Language header parsing | Pairs with @formatjs/intl-localematcher |
+
+### Alternatives Considered
+| Instead of | Could Use | Tradeoff |
+|------------|-----------|----------|
+| next-intl | next-i18next | next-i18next targets Pages Router; SSR config is more complex with App Router |
+| next-intl | Built-in Next.js i18n (no library) | Next.js built-in only does routing; you must build all translation utilities yourself |
+| next-intl | react-i18next | Pure client-side; no Server Component support; larger bundle |
+| JSONB translations column | Separate `agent_template_translations` table | Join table is cleaner at scale but overkill for 7 static templates in 3 languages |
+
+**Installation:**
+```bash
+npm install next-intl @formatjs/intl-localematcher negotiator
+npm install --save-dev @types/negotiator
+```
+(Run in `packages/portal/`)
+
+---
+
+## Architecture Patterns
+
+### Recommended Project Structure
+
+```
+packages/portal/
+├── messages/
+│ ├── en.json # English (source of truth)
+│ ├── es.json # Spanish
+│ └── pt.json # Portuguese
+├── i18n/
+│ └── request.ts # next-intl server-side config (reads cookie + DB)
+├── app/
+│ ├── layout.tsx # Wraps with NextIntlClientProvider
+│ └── (dashboard)/
+│ └── layout.tsx # Already exists — add locale sync here
+├── components/
+│ ├── nav.tsx # Add LanguageSwitcher near user avatar section
+│ ├── language-switcher.tsx # New component
+│ └── ...
+└── next.config.ts # Add withNextIntl wrapper
+```
+
+### Pattern 1: next-intl Without URL Routing
+
+**What:** Locale stored in cookie (pre-auth) and `portal_users.language` DB column (post-auth). No `/en/` prefix in URLs. All routes stay as-is.
+**When to use:** Portal is an authenticated admin tool — SEO locale URLs have no value here. User preference follows them across devices via DB.
+
+```typescript
+// Source: https://next-intl.dev/docs/getting-started/app-router/without-i18n-routing
+// i18n/request.ts
+import { getRequestConfig } from 'next-intl/server';
+import { cookies } from 'next/headers';
+
+const SUPPORTED_LOCALES = ['en', 'es', 'pt'] as const;
+type Locale = typeof SUPPORTED_LOCALES[number];
+
+function isValidLocale(l: string): l is Locale {
+ return (SUPPORTED_LOCALES as readonly string[]).includes(l);
+}
+
+export default getRequestConfig(async () => {
+ const store = await cookies();
+ const raw = store.get('locale')?.value ?? 'en';
+ const locale: Locale = isValidLocale(raw) ? raw : 'en';
+
+ return {
+ locale,
+ messages: (await import(`../messages/${locale}.json`)).default,
+ };
+});
+```
+
+### Pattern 2: next.config.ts Plugin Integration
+
+**What:** The next-intl SWC plugin enables `useTranslations` in Server Components.
+**When to use:** Always — required for SSR-side translation.
+
+```typescript
+// Source: https://next-intl.dev/docs/getting-started/app-router/without-i18n-routing
+// next.config.ts
+import createNextIntlPlugin from 'next-intl/plugin';
+const withNextIntl = createNextIntlPlugin('./i18n/request.ts');
+export default withNextIntl({ output: 'standalone' });
+```
+
+### Pattern 3: Translation in Server and Client Components
+
+**What:** `useTranslations` works identically in Server and Client Components. Server Components get context from `i18n/request.ts`; Client Components from `NextIntlClientProvider`.
+
+```typescript
+// Source: https://next-intl.dev/docs/environments/server-client-components
+
+// Server Component (any page or layout — no 'use client')
+import { useTranslations } from 'next-intl';
+export default function DashboardPage() {
+ const t = useTranslations('dashboard');
+ return
{t('title')}
;
+}
+
+// Client Component (has 'use client')
+'use client';
+import { useTranslations } from 'next-intl';
+export function Nav() {
+ const t = useTranslations('nav');
+ return {t('employees')};
+}
+```
+
+Root layout must wrap with `NextIntlClientProvider`:
+```typescript
+// app/layout.tsx
+import { NextIntlClientProvider } from 'next-intl';
+import { getLocale, getMessages } from 'next-intl/server';
+
+export default async function RootLayout({ children }: { children: React.ReactNode }) {
+ const locale = await getLocale();
+ const messages = await getMessages();
+
+ return (
+
+
+
+ {children}
+
+
+
+ );
+}
+```
+
+**IMPORTANT:** `app/layout.tsx` is currently a Server Component. The dashboard layout `app/(dashboard)/layout.tsx` is a Client Component (`'use client'`). The `NextIntlClientProvider` must go in `app/layout.tsx` (the server root) and wrap the `body`. The dashboard layout is already inside this tree.
+
+### Pattern 4: Language Switcher With DB Persistence
+
+**What:** User picks language in sidebar. A Server Action (or API route) PATCHes `portal_users.language` in DB and sets the `locale` cookie. Then `router.refresh()` forces next-intl to re-read.
+
+```typescript
+// components/language-switcher.tsx
+'use client';
+import { useRouter } from 'next/navigation';
+
+const LOCALES = [
+ { code: 'en', label: 'EN' },
+ { code: 'es', label: 'ES' },
+ { code: 'pt', label: 'PT' },
+] as const;
+
+export function LanguageSwitcher() {
+ const router = useRouter();
+
+ async function handleChange(locale: string) {
+ // Set cookie immediately for fast feedback
+ document.cookie = `locale=${locale}; path=/; max-age=31536000`;
+ // Persist to DB if authenticated
+ await fetch('/api/portal/users/me/language', {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ language: locale }),
+ });
+ // Re-render with new locale
+ router.refresh();
+ }
+
+ return (/* EN / ES / PT buttons */);
+}
+```
+
+### Pattern 5: Login Page Pre-Auth Locale
+
+**What:** Before auth, use `navigator.language` (browser API) to detect initial locale, store in cookie. Language switcher on login page only updates the cookie (no DB PATCH needed — no session yet).
+
+```typescript
+// Detect browser locale on first visit, store as cookie
+const browserLocale = navigator.language.slice(0, 2); // 'es-MX' → 'es'
+const supported = ['en', 'es', 'pt'];
+const detected = supported.includes(browserLocale) ? browserLocale : 'en';
+```
+
+### Pattern 6: Auth.js Language Sync
+
+**What:** On login, the `authorize()` callback fetches user from `/api/portal/auth/verify`. That endpoint should include `language` in the response. The JWT carries `language` for fast client-side access (avoids DB round-trip on every render). When user changes language, `update()` session trigger updates the JWT.
+
+The JWT update pattern already exists for `active_tenant_id`. Language follows the same pattern.
+
+### Pattern 7: Agent Template Translations (DB)
+
+**What:** A `translations` JSONB column on `agent_templates` stores locale-keyed objects for name, description, and persona.
+
+```sql
+-- Migration 009 adds:
+ALTER TABLE agent_templates ADD COLUMN translations JSONB NOT NULL DEFAULT '{}';
+
+-- Example stored value:
+{
+ "es": {
+ "name": "Representante de Atención al Cliente",
+ "description": "Un agente de soporte profesional...",
+ "persona": "Eres profesional, empático y orientado a soluciones..."
+ },
+ "pt": {
+ "name": "Representante de Suporte ao Cliente",
+ "description": "Um agente de suporte profissional...",
+ "persona": "Você é profissional, empático e orientado a soluções..."
+ }
+}
+```
+
+The API endpoint `GET /api/portal/templates` should accept a `?locale=es` query param and merge translated fields before returning.
+
+### Pattern 8: AI Employee Language Instruction
+
+**What:** A single sentence appended to the system prompt (after the AI transparency clause or integrated before it).
+
+```python
+# packages/shared/shared/prompts/system_prompt_builder.py
+LANGUAGE_INSTRUCTION = (
+ "Detect the language of each user message and respond in that same language. "
+ "You support English, Spanish, and Portuguese."
+)
+```
+
+This is appended unconditionally to all agent system prompts — no config flag needed. The instruction is concise and effective: modern LLMs follow it reliably across Claude, GPT-4, and Ollama/Qwen models.
+
+### Pattern 9: JSON Message File Structure
+
+**What:** Nested JSON keyed by component/feature area. Flat keys within each namespace.
+
+```json
+// messages/en.json
+{
+ "nav": {
+ "dashboard": "Dashboard",
+ "employees": "Employees",
+ "chat": "Chat",
+ "usage": "Usage",
+ "billing": "Billing",
+ "apiKeys": "API Keys",
+ "users": "Users",
+ "platform": "Platform",
+ "signOut": "Sign out"
+ },
+ "login": {
+ "title": "Welcome back",
+ "emailLabel": "Email",
+ "passwordLabel": "Password",
+ "submitButton": "Sign in",
+ "invalidCredentials": "Invalid email or password. Please try again."
+ },
+ "agents": {
+ "pageTitle": "AI Employees",
+ "newEmployee": "New Employee",
+ "noAgents": "No employees yet",
+ "deployButton": "Deploy",
+ "editButton": "Edit"
+ }
+ // ... one key group per page/component
+}
+```
+
+### Anti-Patterns to Avoid
+
+- **Locale in URLs for an authenticated portal:** `/en/dashboard` adds no SEO value and requires restructuring every route segment. Cookie + DB is the correct approach here.
+- **Translating backend error messages:** Backend returns error codes (`"error_code": "AGENT_NOT_FOUND"`), frontend translates to display text. Backend error messages are never user-facing directly.
+- **Separate translations table for 7 templates:** A `translations` JSONB column is sufficient. A join table is premature over-engineering for this use case.
+- **Putting `NextIntlClientProvider` in the dashboard layout:** It must go in `app/layout.tsx` (the server root), not in the client dashboard layout, so server components throughout the tree can use `useTranslations`.
+- **Using `getTranslations` in Client Components:** `getTranslations` is async and for Server Components. Client Components must use the `useTranslations` hook — they receive messages via `NextIntlClientProvider` context.
+- **Hard-coding the `locale` cookie key in multiple places:** Define it as a constant (`LOCALE_COOKIE_NAME = 'konstruct_locale'`) used everywhere.
+
+---
+
+## Don't Hand-Roll
+
+| Problem | Don't Build | Use Instead | Why |
+|---------|-------------|-------------|-----|
+| Translation lookup + fallback | Custom `t()` function | `useTranslations` (next-intl) | Missing key fallback, TypeScript key safety, pluralization rules, ICU format support |
+| Browser locale negotiation | `navigator.language` + manual mapping | `@formatjs/intl-localematcher` + `negotiator` | RFC 5646 language tag parsing, quality values (q=0.9), region fallback (es-MX → es) |
+| Date/number/currency formatting | `new Intl.DateTimeFormat(...)` manually | `useFormatter` (next-intl) | Consistent locale-aware formatting tied to current locale context |
+| SSR hydration mismatch for locale | Manual hydration logic | next-intl `NextIntlClientProvider` | Ensures server and client render identically; prevents React hydration warnings |
+
+**Key insight:** Translation systems have invisible complexity — plural forms, ICU message syntax, TypeScript key checking, fallback chains. next-intl handles all of these; hand-rolled solutions always miss edge cases.
+
+---
+
+## Common Pitfalls
+
+### Pitfall 1: `app/layout.tsx` Is a Server Component — Don't Make It a Client Component
+
+**What goes wrong:** Developer adds `NextIntlClientProvider` (correct) but also adds `'use client'` (wrong). This prevents the provider from reading server-side messages.
+**Why it happens:** Developer sees `NextIntlClientProvider` and assumes it needs a client wrapper.
+**How to avoid:** `app/layout.tsx` stays a Server Component. Call `getLocale()` and `getMessages()` (async server functions) then pass results as props to `NextIntlClientProvider`.
+**Warning signs:** TypeScript error `Cannot use import statement outside a module` or translations missing in SSR output.
+
+### Pitfall 2: `router.refresh()` vs. `router.push()` for Language Change
+
+**What goes wrong:** Using `router.push('/')` after language change navigates to home page instead of re-rendering current page with new locale.
+**Why it happens:** Developer treats language change like navigation.
+**How to avoid:** Use `router.refresh()` — it re-executes Server Components on the current URL, causing next-intl to re-read the updated cookie.
+**Warning signs:** Language appears to change but user is redirected to dashboard regardless of which page they were on.
+
+### Pitfall 3: Cookie Not Set Before First Render
+
+**What goes wrong:** Login page shows English even when user's browser is Spanish, because the cookie doesn't exist yet on first visit.
+**Why it happens:** Cookie is only set when user explicitly picks a language.
+**How to avoid:** In the login page, use `navigator.language` (client-side) to detect browser locale on mount and set the `locale` cookie via JavaScript before the language switcher renders. Or handle this in `i18n/request.ts` by falling back to the `Accept-Language` header.
+**Warning signs:** Pre-auth language switcher ignores browser locale on first visit.
+
+### Pitfall 4: Auth.js JWT Does Not Auto-Update After Language Change
+
+**What goes wrong:** User changes language. Cookie updates. But `session.user.language` in JWT still shows old value until re-login.
+**Why it happens:** JWT is stateless — it only updates when `update()` is called or on re-login.
+**How to avoid:** Call `update({ language: newLocale })` from the LanguageSwitcher component after the DB PATCH succeeds. This triggers the Auth.js `jwt` callback with `trigger="update"` and updates the token in-place (same pattern already used for `active_tenant_id`).
+**Warning signs:** `session.user.language` returns stale value after language switch; `router.refresh()` doesn't update language because the JWT callback is still reading the old value.
+
+### Pitfall 5: next-intl v4 Breaking Change — `NextIntlClientProvider` Is Now Required
+
+**What goes wrong:** Client Components using `useTranslations` throw errors in v4 if `NextIntlClientProvider` is not in the tree.
+**Why it happens:** v4 removed the implicit provider behavior that v3 had. The requirement became explicit.
+**How to avoid:** Ensure `NextIntlClientProvider` wraps the entire app in `app/layout.tsx`, NOT just the dashboard layout.
+**Warning signs:** `Error: Could not find NextIntlClientProvider` in browser console.
+
+### Pitfall 6: Template Translations Not Applied When Portal Language Changes
+
+**What goes wrong:** User switches to Spanish but template names/descriptions still show English.
+**Why it happens:** The `/api/portal/templates` endpoint returns raw DB data without locale-merging.
+**How to avoid:** The templates API endpoint must accept a `?locale=es` param and merge the `translations[locale]` fields over the base English fields before returning to the client.
+**Warning signs:** Template gallery shows English names after language switch.
+
+### Pitfall 7: Session-Scoped Cookie (next-intl v4 Default)
+
+**What goes wrong:** User picks Spanish. Closes browser. Reopens and sees English.
+**Why it happens:** next-intl v4 changed the locale cookie to session-scoped by default (GDPR compliance). The cookie expires when the browser closes.
+**How to avoid:** For this portal, set an explicit cookie expiry of 1 year (`max-age=31536000`) in the LanguageSwitcher when writing the `locale` cookie directly. The DB-persisted preference is the authoritative source; cookie is the fast path. `i18n/request.ts` should also read the DB value from the session token if available.
+**Warning signs:** Language resets to default on every browser restart.
+
+### Pitfall 8: `next-intl` Plugin Missing From `next.config.ts`
+
+**What goes wrong:** `useTranslations` throws at runtime in Server Components.
+**Why it happens:** Without the SWC plugin, the async context required for server-side translations is not set up.
+**How to avoid:** Wrap the config with `withNextIntl` in `next.config.ts` — required step, not optional.
+**Warning signs:** `Error: No request found. Please check that you've set up "next-intl" correctly.`
+
+---
+
+## Code Examples
+
+Verified patterns from official sources:
+
+### Complete `i18n/request.ts` (Without URL Routing)
+```typescript
+// Source: https://next-intl.dev/docs/getting-started/app-router/without-i18n-routing
+import { getRequestConfig } from 'next-intl/server';
+import { cookies } from 'next/headers';
+
+const SUPPORTED_LOCALES = ['en', 'es', 'pt'] as const;
+export type Locale = typeof SUPPORTED_LOCALES[number];
+
+export function isValidLocale(l: string): l is Locale {
+ return (SUPPORTED_LOCALES as readonly string[]).includes(l);
+}
+
+export const LOCALE_COOKIE = 'konstruct_locale';
+
+export default getRequestConfig(async () => {
+ const store = await cookies();
+ const raw = store.get(LOCALE_COOKIE)?.value ?? 'en';
+ const locale: Locale = isValidLocale(raw) ? raw : 'en';
+
+ return {
+ locale,
+ messages: (await import(`../messages/${locale}.json`)).default,
+ };
+});
+```
+
+### `app/layout.tsx` Root Layout with Provider
+```typescript
+// Source: https://next-intl.dev/docs/getting-started/app-router/without-i18n-routing
+import { NextIntlClientProvider } from 'next-intl';
+import { getLocale, getMessages } from 'next-intl/server';
+import type { Metadata } from 'next';
+import { DM_Sans, JetBrains_Mono } from 'next/font/google';
+import './globals.css';
+
+export default async function RootLayout({ children }: { children: React.ReactNode }) {
+ const locale = await getLocale();
+ const messages = await getMessages();
+
+ return (
+
+
+
+ {children}
+
+
+
+ );
+}
+```
+
+### System Prompt Language Instruction
+```python
+# packages/shared/shared/prompts/system_prompt_builder.py
+LANGUAGE_INSTRUCTION = (
+ "Detect the language of each user message and respond in that same language. "
+ "You support English, Spanish, and Portuguese."
+)
+
+# In build_system_prompt(), append before or after AI_TRANSPARENCY_CLAUSE:
+sections.append(LANGUAGE_INSTRUCTION)
+sections.append(AI_TRANSPARENCY_CLAUSE)
+```
+
+### Invitation Email With Language Parameter
+```python
+# packages/shared/shared/email.py
+def send_invite_email(
+ to_email: str,
+ invitee_name: str,
+ tenant_name: str,
+ invite_url: str,
+ language: str = "en", # New parameter — inviter's current language
+) -> None:
+ ...
+ subjects = {
+ "en": f"You've been invited to join {tenant_name} on Konstruct",
+ "es": f"Has sido invitado a unirte a {tenant_name} en Konstruct",
+ "pt": f"Você foi convidado para entrar em {tenant_name} no Konstruct",
+ }
+ subject = subjects.get(language, subjects["en"])
+ ...
+```
+
+### Migration 009 — Language Field + Template Translations
+```python
+# migrations/versions/009_multilanguage.py
+def upgrade() -> None:
+ # Add language preference to portal_users
+ op.add_column('portal_users',
+ sa.Column('language', sa.String(10), nullable=False, server_default='en')
+ )
+
+ # Add translations JSONB to agent_templates
+ op.add_column('agent_templates',
+ sa.Column('translations', sa.JSON, nullable=False, server_default='{}')
+ )
+
+ # Backfill translations for all 7 existing templates
+ conn = op.get_bind()
+ # ... UPDATE agent_templates SET translations = :translations WHERE id = :id
+ # for each of the 7 templates with ES + PT translations
+```
+
+---
+
+## State of the Art
+
+| Old Approach | Current Approach | When Changed | Impact |
+|--------------|------------------|--------------|--------|
+| next-i18next | next-intl | 2022-2023 | next-i18next is Pages Router-centric; next-intl is the App Router standard |
+| URL-prefixed locale routing (`/en/`) | Cookie/DB locale without URL prefix | Recognized pattern 2024+ | Authenticated portals don't need SEO locale URLs |
+| next-intl v3 implicit provider | next-intl v4 explicit `NextIntlClientProvider` required | Feb 2026 (v4.0) | Must wrap all client components explicitly |
+| next-intl session cookie | next-intl v4 session cookie (GDPR) | Feb 2026 (v4.0) | Cookie now expires with browser; must set `max-age` manually for persistence |
+
+**Deprecated/outdated:**
+- `middleware.ts` in Next.js: renamed to `proxy.ts` in Next.js 16 (already done in this codebase)
+- `zodResolver` from hookform/resolvers: replaced with `standardSchemaResolver` (already done — this is relevant because Zod error messages also need i18n)
+
+---
+
+## Open Questions
+
+1. **Template Translation Quality**
+ - What we know: User wants templates that "feel native, not machine-translated"
+ - What's unclear: Will Claude generate the translations during migration seeding, or will they be written manually?
+ - Recommendation: Planner should include a task note that the Spanish and Portuguese template translations must be human-reviewed; generate initial translations with Claude then have a native speaker verify before production deploy.
+
+2. **Zod Validation Messages**
+ - What we know: Zod v4 supports custom error maps; existing forms use `standardSchemaResolver`
+ - What's unclear: Zod v4's i18n integration path is not documented in the searched sources
+ - Recommendation: Pass translated error strings directly in the Zod schema definitions rather than using a global error map — e.g., `z.string().email(t('validation.invalidEmail'))`. This is simpler and works with existing `standardSchemaResolver`.
+
+3. **Session Language Sync Timing**
+ - What we know: The JWT `update()` pattern works for `active_tenant_id`; same pattern applies to `language`
+ - What's unclear: Whether `getLocale()` in `i18n/request.ts` should prefer the JWT session value or the cookie value when both are available
+ - Recommendation: Priority order: DB/JWT value (most authoritative) → cookie → default `'en'`. Implement by reading the Auth.js session inside `getRequestConfig` and preferring `session.user.language` over cookie.
+
+---
+
+## Validation Architecture
+
+### Test Framework
+| Property | Value |
+|----------|-------|
+| Framework | pytest 8.3+ with pytest-asyncio |
+| Config file | `pyproject.toml` (`[tool.pytest.ini_options]`) |
+| Quick run command | `pytest tests/unit/test_system_prompt_builder.py -x` |
+| Full suite command | `pytest tests/unit -x` |
+
+### Phase Requirements → Test Map
+| Req ID | Behavior | Test Type | Automated Command | File Exists? |
+|--------|----------|-----------|-------------------|-------------|
+| I18N-03 | `build_system_prompt()` appends language instruction | unit | `pytest tests/unit/test_system_prompt_builder.py -x` | ✅ (extend existing) |
+| I18N-03 | Language instruction present on all code paths (full, minimal, empty args) | unit | `pytest tests/unit/test_system_prompt_builder.py::TestLanguageInstruction -x` | ❌ Wave 0 |
+| I18N-06 | `isValidLocale()` returns false for unsupported locales, true for en/es/pt | unit | `pytest tests/unit/test_i18n_utils.py -x` | ❌ Wave 0 |
+| I18N-01 | Portal UI loads in Spanish when cookie is `es` | manual-only | N/A — requires browser render | — |
+| I18N-02 | Language switcher PATCH updates `portal_users.language` | integration | `pytest tests/integration/test_language_preference.py -x` | ❌ Wave 0 |
+| I18N-04 | Templates API returns translated fields when `?locale=es` | integration | `pytest tests/integration/test_templates_i18n.py -x` | ❌ Wave 0 |
+| I18N-05 | Login form shows Spanish error on invalid credentials when locale=es | manual-only | N/A — requires browser render | — |
+
+**Note:** I18N-01 and I18N-05 are frontend-only concerns (rendered TSX). They are verifiable by human testing in the browser and do not lend themselves to automated unit/integration tests without a full browser automation setup (Playwright), which is not in the current test infrastructure.
+
+### Sampling Rate
+- **Per task commit:** `pytest tests/unit/test_system_prompt_builder.py -x`
+- **Per wave merge:** `pytest tests/unit -x`
+- **Phase gate:** Full suite green before `/gsd:verify-work`
+
+### Wave 0 Gaps
+- [ ] `tests/unit/test_system_prompt_builder.py` — extend with `TestLanguageInstruction` class covering I18N-03 language instruction on all `build_system_prompt()` call paths
+- [ ] `tests/integration/test_language_preference.py` — covers I18N-02 PATCH language endpoint
+- [ ] `tests/integration/test_templates_i18n.py` — covers I18N-04 locale-aware templates endpoint
+
+---
+
+## Sources
+
+### Primary (HIGH confidence)
+- Next.js 16 official docs (local): `packages/portal/node_modules/next/dist/docs/01-app/02-guides/internationalization.md` — Next.js i18n guide, routing patterns, proxy.ts locale detection
+- next-intl official docs (WebFetch): https://next-intl.dev/docs/getting-started/app-router/without-i18n-routing — complete without-routing setup
+- next-intl official docs (WebFetch): https://next-intl.dev/docs/environments/server-client-components — server vs client component translation patterns
+- next-intl v4 changelog (WebFetch): https://next-intl.dev/blog/next-intl-4-0 — breaking changes including mandatory `NextIntlClientProvider` and session cookie change
+- GitHub (WebFetch): https://github.com/amannn/next-intl — confirmed v4.8.3 as of 2026-02-16
+- next-intl without-routing cookie example (WebFetch): https://next-intl.dev/docs/usage/configuration
+
+### Secondary (MEDIUM confidence)
+- Blog guide for without-routing implementation: https://jb.desishub.com/blog/nextjs-i18n-docs — verified against official docs
+- WebSearch 2026 ecosystem survey confirming next-intl as the App Router standard
+
+### Tertiary (LOW confidence)
+- Python language detection library comparison for AI agent language detection fallback (websearch only) — not required for v1 (system prompt handles detection at LLM level)
+
+---
+
+## Metadata
+
+**Confidence breakdown:**
+- Standard stack (next-intl v4): HIGH — confirmed via official Next.js docs listing it first + version confirmed from GitHub + docs read directly
+- Architecture (without-routing pattern): HIGH — official next-intl docs page read directly via WebFetch
+- next-intl v4 breaking changes: HIGH — read from official changelog
+- Pitfalls: HIGH for pitfalls 1–5 (all sourced from official docs); MEDIUM for pitfalls 6–8 (derived from architecture + v4 changelog)
+- Template translations approach: HIGH — standard JSONB pattern, well-established PostgreSQL convention
+- AI agent language instruction: HIGH — system prompt approach is simple and confirmed effective by industry research
+
+**Research date:** 2026-03-25
+**Valid until:** 2026-06-25 (next-intl fast-moving but stable API; 90 days)