Compare commits

...

4 Commits

7 changed files with 1728 additions and 4 deletions

View File

@@ -141,7 +141,7 @@ Phases execute in numeric order: 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7
| 4. RBAC | 3/3 | Complete | 2026-03-24 |
| 5. Employee Design | 4/4 | Complete | 2026-03-25 |
| 6. Web Chat | 3/3 | Complete | 2026-03-25 |
| 7. Multilanguage | 0/0 | Not started | - |
| 7. Multilanguage | 0/4 | Not started | - |
---
@@ -160,11 +160,14 @@ Phases execute in numeric order: 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7
4. Agent templates, wizard steps, and onboarding flow are all fully translated
5. Error messages, validation text, and system notifications are localized
6. Adding a new language in the future requires only adding translation files, not code changes
**Plans**: 0 plans
**Plans**: 4 plans
Plans:
- [ ] TBD (run /gsd:plan-phase 7 to break down)
- [ ] 07-01-PLAN.md — Backend i18n: migration 009 (language column + translations JSONB), system prompt language instruction, localized emails, locale-aware templates API
- [ ] 07-02-PLAN.md — Frontend i18n infrastructure: next-intl setup, complete en/es/pt message files, language switcher, Auth.js JWT language sync
- [ ] 07-03-PLAN.md — Frontend string extraction: replace all hardcoded English strings with useTranslations() calls across all pages and components
- [ ] 07-04-PLAN.md — Human verification: multilanguage testing across all pages, language switcher, AI Employee language response
---
*Roadmap created: 2026-03-23*
*Coverage: 25/25 v1 requirements + 6 RBAC requirements + 5 Employee Design requirements + 5 Web Chat requirements mapped*
*Coverage: 25/25 v1 requirements + 6 RBAC requirements + 5 Employee Design requirements + 5 Web Chat requirements + 6 Multilanguage requirements mapped*

View File

@@ -0,0 +1,288 @@
---
phase: 07-multilanguage
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- migrations/versions/009_multilanguage.py
- packages/shared/shared/models/tenant.py
- packages/shared/shared/models/auth.py
- packages/shared/shared/prompts/system_prompt_builder.py
- packages/portal/lib/system-prompt-builder.ts
- packages/shared/shared/email.py
- packages/shared/shared/api/templates.py
- packages/shared/shared/api/portal.py
- tests/unit/test_system_prompt_builder.py
- tests/integration/test_language_preference.py
- tests/integration/test_templates_i18n.py
autonomous: true
requirements:
- I18N-03
- I18N-04
- I18N-05
- I18N-06
must_haves:
truths:
- "AI Employees respond in the same language the user writes in"
- "Agent templates have Spanish and Portuguese translations stored in DB"
- "Invitation emails are sent in the inviting admin's language"
- "portal_users table has a language column defaulting to 'en'"
- "Templates API returns translated fields when locale param is provided"
artifacts:
- path: "migrations/versions/009_multilanguage.py"
provides: "DB migration adding language to portal_users and translations JSONB to agent_templates"
contains: "portal_users"
- path: "packages/shared/shared/prompts/system_prompt_builder.py"
provides: "Language instruction appended to all system prompts"
contains: "LANGUAGE_INSTRUCTION"
- path: "packages/portal/lib/system-prompt-builder.ts"
provides: "TS mirror with language instruction"
contains: "LANGUAGE_INSTRUCTION"
- path: "packages/shared/shared/email.py"
provides: "Localized invitation emails in en/es/pt"
contains: "language"
- path: "packages/shared/shared/api/templates.py"
provides: "Locale-aware template list endpoint"
contains: "locale"
- path: "tests/integration/test_language_preference.py"
provides: "Integration tests for PATCH language preference endpoint"
contains: "test_"
- path: "tests/integration/test_templates_i18n.py"
provides: "Integration tests for locale-aware templates endpoint"
contains: "test_"
key_links:
- from: "packages/shared/shared/prompts/system_prompt_builder.py"
to: "AI Employee responses"
via: "LANGUAGE_INSTRUCTION appended in build_system_prompt()"
pattern: "LANGUAGE_INSTRUCTION"
- from: "packages/shared/shared/api/templates.py"
to: "agent_templates.translations"
via: "JSONB column merge on locale query param"
pattern: "translations"
---
<objective>
Backend multilanguage foundation: DB migration, system prompt language instruction, localized invitation emails, and locale-aware templates API.
Purpose: Provides the backend data layer and AI language behavior that all frontend i18n depends on. Without this, there is no language column to persist, no template translations to display, and no agent language instruction.
Output: Migration 009, updated system prompt builder (Python + TS), localized email sender, locale-aware templates API, unit tests, integration tests.
</objective>
<execution_context>
@/home/adelorenzo/.claude/get-shit-done/workflows/execute-plan.md
@/home/adelorenzo/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/07-multilanguage/07-CONTEXT.md
@.planning/phases/07-multilanguage/07-RESEARCH.md
<interfaces>
<!-- Key types and contracts the executor needs -->
From packages/shared/shared/models/auth.py:
```python
class PortalUser(Base):
__tablename__ = "portal_users"
id: Mapped[uuid.UUID]
email: Mapped[str]
hashed_password: Mapped[str]
name: Mapped[str]
role: Mapped[str]
created_at: Mapped[datetime]
updated_at: Mapped[datetime]
# NEEDS: language: Mapped[str] = mapped_column(String(10), nullable=False, server_default='en')
```
From packages/shared/shared/models/tenant.py (AgentTemplate):
```python
class AgentTemplate(Base):
__tablename__ = "agent_templates"
# Existing columns: id, name, role, description, category, persona, system_prompt,
# model_preference, tool_assignments, escalation_rules, is_active, sort_order, created_at
# NEEDS: translations: Mapped[dict] = mapped_column(JSON, nullable=False, server_default='{}')
```
From packages/shared/shared/prompts/system_prompt_builder.py:
```python
AI_TRANSPARENCY_CLAUSE = "When directly asked if you are an AI, always disclose that you are an AI assistant."
def build_system_prompt(name, role, persona="", tool_assignments=None, escalation_rules=None) -> str
```
From packages/portal/lib/system-prompt-builder.ts:
```typescript
export interface SystemPromptInput { name: string; role: string; persona?: string; ... }
export function buildSystemPrompt(data: SystemPromptInput): string
```
From packages/shared/shared/email.py:
```python
def send_invite_email(to_email: str, invitee_name: str, tenant_name: str, invite_url: str) -> None
# NEEDS: language: str = "en" parameter added
```
From packages/shared/shared/api/templates.py:
```python
class TemplateResponse(BaseModel):
id: str; name: str; role: str; description: str; category: str; persona: str; ...
@classmethod
def from_orm(cls, tmpl: AgentTemplate) -> "TemplateResponse"
@templates_router.get("/templates", response_model=list[TemplateResponse])
async def list_templates(caller, session) -> list[TemplateResponse]
# NEEDS: locale: str = Query("en") parameter, merge translations before returning
```
From migrations/versions/ — latest is 008_web_chat.py, so next is 009.
</interfaces>
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: DB migration 009 + ORM updates + system prompt language instruction</name>
<files>
migrations/versions/009_multilanguage.py
packages/shared/shared/models/auth.py
packages/shared/shared/models/tenant.py
packages/shared/shared/prompts/system_prompt_builder.py
packages/portal/lib/system-prompt-builder.ts
tests/unit/test_system_prompt_builder.py
</files>
<behavior>
- Test: build_system_prompt("Mara", "Support Rep") output contains LANGUAGE_INSTRUCTION string
- Test: build_system_prompt with full args (persona, tools, escalation) contains LANGUAGE_INSTRUCTION
- Test: build_system_prompt with minimal args (name, role only) contains LANGUAGE_INSTRUCTION
- Test: LANGUAGE_INSTRUCTION appears after identity section and before AI_TRANSPARENCY_CLAUSE
</behavior>
<action>
1. Create migration 009_multilanguage.py:
- Add `language` column (String(10), NOT NULL, server_default='en') to portal_users
- Add `translations` column (JSON, NOT NULL, server_default='{}') to agent_templates
- Backfill translations for all 7 existing seed templates with Spanish (es) and Portuguese (pt) translations for name, description, and persona fields. Use proper native business terminology — not literal machine translations. Each template gets a translations JSON object like: {"es": {"name": "...", "description": "...", "persona": "..."}, "pt": {"name": "...", "description": "...", "persona": "..."}}
- downgrade: drop both columns
2. Update ORM models:
- PortalUser: add `language: Mapped[str] = mapped_column(String(10), nullable=False, server_default='en')`
- AgentTemplate: add `translations: Mapped[dict] = mapped_column(JSON, nullable=False, server_default='{}')`
3. Add LANGUAGE_INSTRUCTION to system_prompt_builder.py:
```python
LANGUAGE_INSTRUCTION = (
"Detect the language of each user message and respond in that same language. "
"You support English, Spanish, and Portuguese."
)
```
Append to sections list BEFORE AI_TRANSPARENCY_CLAUSE (transparency clause remains last).
4. Add LANGUAGE_INSTRUCTION to system-prompt-builder.ts (TS mirror):
```typescript
const LANGUAGE_INSTRUCTION = "Detect the language of each user message and respond in that same language. You support English, Spanish, and Portuguese.";
```
Append before the AI transparency clause line.
5. Extend tests/unit/test_system_prompt_builder.py with TestLanguageInstruction class:
- test_language_instruction_present_in_default_prompt
- test_language_instruction_present_with_full_args
- test_language_instruction_before_transparency_clause
</action>
<verify>
<automated>cd /home/adelorenzo/repos/konstruct && python -m pytest tests/unit/test_system_prompt_builder.py -x -v</automated>
</verify>
<done>
- Migration 009 creates language column on portal_users and translations JSONB on agent_templates with es+pt seed data
- LANGUAGE_INSTRUCTION appears in all system prompts (Python and TS)
- All existing + new system prompt tests pass
</done>
</task>
<task type="auto">
<name>Task 2: Localized invitation emails + locale-aware templates API + language preference endpoint + integration tests</name>
<files>
packages/shared/shared/email.py
packages/shared/shared/api/templates.py
packages/shared/shared/api/portal.py
tests/integration/test_language_preference.py
tests/integration/test_templates_i18n.py
</files>
<action>
1. Update send_invite_email() in email.py:
- Add `language: str = "en"` parameter
- Create localized subject lines dict: {"en": "You've been invited...", "es": "Has sido invitado...", "pt": "Voce foi convidado..."}
- Create localized text_body and html_body templates for all 3 languages
- Select the correct template based on language param, fallback to "en"
- Update the invitations API endpoint that calls send_invite_email to pass the inviter's language (read from portal_users.language or default "en")
2. Update templates API (templates.py):
- Add `locale: str = Query("en")` parameter to list_templates() and get_template()
- In TemplateResponse.from_orm(), add a `locale` parameter
- When locale != "en" and tmpl.translations has the locale key, merge translated name/description/persona over the English defaults before returning
- Keep English as the base — translations overlay, never replace the stored English values in DB
3. Add language preference PATCH endpoint in portal.py:
- PATCH /api/portal/users/me/language — accepts {"language": "es"} body
- Validates language is in ["en", "es", "pt"]
- Updates portal_users.language for the current user
- Returns {"language": "es"} on success
- Guard: any authenticated user (get_portal_caller)
4. Update the verify auth endpoint (/api/portal/auth/verify) to include `language` in its response so Auth.js JWT can carry it.
5. Create tests/integration/test_language_preference.py (Wave 0 — I18N-02):
- Use the existing integration test pattern (httpx AsyncClient against the FastAPI app)
- test_patch_language_valid: PATCH /api/portal/users/me/language with {"language": "es"} returns 200 and {"language": "es"}
- test_patch_language_invalid: PATCH with {"language": "fr"} returns 422 or 400
- test_patch_language_persists: PATCH to "pt", then GET /api/portal/auth/verify includes language="pt"
- test_patch_language_unauthenticated: PATCH without auth returns 401
6. Create tests/integration/test_templates_i18n.py (Wave 0 — I18N-04):
- Use the existing integration test pattern (httpx AsyncClient against the FastAPI app)
- test_list_templates_default_locale: GET /api/portal/templates returns English fields (no locale param)
- test_list_templates_spanish: GET /api/portal/templates?locale=es returns Spanish-translated name/description/persona
- test_list_templates_portuguese: GET /api/portal/templates?locale=pt returns Portuguese-translated fields
- test_list_templates_unsupported_locale: GET /api/portal/templates?locale=fr falls back to English
- test_template_translations_overlay: Verify translated fields overlay English, not replace — English base fields still accessible in DB
</action>
<verify>
<automated>cd /home/adelorenzo/repos/konstruct && python -m pytest tests/unit -x -q && python -m pytest tests/integration/test_language_preference.py tests/integration/test_templates_i18n.py -x -v</automated>
</verify>
<done>
- send_invite_email() accepts language param and sends localized emails in en/es/pt
- GET /api/portal/templates?locale=es returns Spanish-translated template fields
- PATCH /api/portal/users/me/language persists language preference
- /api/portal/auth/verify response includes user's language field
- Integration tests for language preference endpoint pass (4 tests)
- Integration tests for locale-aware templates endpoint pass (5 tests)
</done>
</task>
</tasks>
<verification>
- All existing unit tests pass: `pytest tests/unit -x -q`
- Integration tests pass: `pytest tests/integration/test_language_preference.py tests/integration/test_templates_i18n.py -x -v`
- Migration 009 is syntactically valid (imports, upgrade/downgrade functions present)
- system_prompt_builder.py contains LANGUAGE_INSTRUCTION
- system-prompt-builder.ts contains LANGUAGE_INSTRUCTION
- email.py send_invite_email has language parameter
- templates.py list_templates has locale parameter
</verification>
<success_criteria>
- Migration 009 adds language to portal_users and translations JSONB to agent_templates
- All 7 seed templates have es+pt translations backfilled
- AI Employees will respond in the user's language via system prompt instruction
- Templates API merges translations by locale
- Language preference PATCH endpoint works
- All unit tests pass
- All integration tests pass (language preference + templates i18n)
</success_criteria>
<output>
After completion, create `.planning/phases/07-multilanguage/07-01-SUMMARY.md`
</output>

View File

@@ -0,0 +1,286 @@
---
phase: 07-multilanguage
plan: 02
type: execute
wave: 1
depends_on: []
files_modified:
- packages/portal/package.json
- packages/portal/next.config.ts
- packages/portal/i18n/request.ts
- packages/portal/messages/en.json
- packages/portal/messages/es.json
- packages/portal/messages/pt.json
- packages/portal/app/layout.tsx
- packages/portal/components/language-switcher.tsx
- packages/portal/components/nav.tsx
- packages/portal/lib/auth.ts
- packages/portal/lib/auth-types.ts
- packages/portal/components/session-sync.tsx
- packages/portal/app/(auth)/login/page.tsx
autonomous: true
requirements:
- I18N-01
- I18N-02
- I18N-06
must_haves:
truths:
- "next-intl is installed and configured with cookie-based locale (no URL routing)"
- "NextIntlClientProvider wraps the app in root layout.tsx"
- "Language switcher is visible in the sidebar near the user avatar"
- "Language selection persists via cookie (pre-auth) and DB (post-auth)"
- "Login page detects browser locale and shows a language switcher"
- "Adding a new language requires only a new JSON file in messages/"
artifacts:
- path: "packages/portal/i18n/request.ts"
provides: "next-intl server config reading locale from cookie"
contains: "getRequestConfig"
- path: "packages/portal/messages/en.json"
provides: "English translation source of truth"
contains: "nav"
- path: "packages/portal/messages/es.json"
provides: "Spanish translations"
contains: "nav"
- path: "packages/portal/messages/pt.json"
provides: "Portuguese translations"
contains: "nav"
- path: "packages/portal/components/language-switcher.tsx"
provides: "Language picker component with EN/ES/PT options"
contains: "LanguageSwitcher"
key_links:
- from: "packages/portal/app/layout.tsx"
to: "packages/portal/i18n/request.ts"
via: "NextIntlClientProvider reads locale + messages from server config"
pattern: "NextIntlClientProvider"
- from: "packages/portal/components/language-switcher.tsx"
to: "/api/portal/users/me/language"
via: "PATCH request to persist language preference"
pattern: "fetch.*language"
- from: "packages/portal/components/nav.tsx"
to: "packages/portal/components/language-switcher.tsx"
via: "LanguageSwitcher rendered in user section of sidebar"
pattern: "LanguageSwitcher"
---
<objective>
Frontend i18n infrastructure: install next-intl, create message files with complete translation keys for all portal pages, configure root layout provider, build language switcher, and integrate with Auth.js JWT for language persistence.
Purpose: Establishes the i18n framework so all portal components can use `useTranslations()`. Creates the complete en/es/pt message files with all translation keys for every page and component. This plan does the infrastructure setup AND the translation file authoring, while Plan 03 does the actual string extraction (replacing hardcoded strings with `t()` calls).
Output: Working next-intl setup, complete message files in 3 languages, language switcher in sidebar and login page.
</objective>
<execution_context>
@/home/adelorenzo/.claude/get-shit-done/workflows/execute-plan.md
@/home/adelorenzo/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/07-multilanguage/07-CONTEXT.md
@.planning/phases/07-multilanguage/07-RESEARCH.md
<interfaces>
<!-- Current root layout (Server Component — must remain Server Component) -->
From packages/portal/app/layout.tsx:
```typescript
// Currently: static html lang="en", no i18n provider
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className="...">
<body className="min-h-full flex flex-col">{children}</body>
</html>
);
}
```
<!-- Nav sidebar (Client Component) — language switcher goes in user section -->
From packages/portal/components/nav.tsx:
```typescript
'use client';
// navItems array has hardcoded labels: "Dashboard", "Employees", "Chat", etc.
// User section at bottom with sign out button
export function Nav() { ... }
```
<!-- Auth.js config — JWT already carries role, active_tenant_id -->
From packages/portal/lib/auth.ts:
```typescript
// JWT callback with trigger="update" pattern used for active_tenant_id
// Same pattern needed for language field
```
<!-- next.config.ts — needs withNextIntl wrapper -->
From packages/portal/next.config.ts:
```typescript
const nextConfig: NextConfig = { output: "standalone" };
export default nextConfig;
// MUST become: export default withNextIntl(nextConfig);
```
<!-- Login page — needs pre-auth language switcher -->
From packages/portal/app/(auth)/login/page.tsx — Client Component with email/password form
<!-- Session sync — syncs RBAC headers, could sync language -->
From packages/portal/components/session-sync.tsx
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Install next-intl, configure i18n infrastructure, create complete message files</name>
<files>
packages/portal/package.json
packages/portal/next.config.ts
packages/portal/i18n/request.ts
packages/portal/messages/en.json
packages/portal/messages/es.json
packages/portal/messages/pt.json
packages/portal/app/layout.tsx
</files>
<action>
1. Install dependencies in packages/portal/:
```bash
cd packages/portal && npm install next-intl @formatjs/intl-localematcher negotiator && npm install --save-dev @types/negotiator
```
2. Create i18n/request.ts (next-intl server config WITHOUT URL routing):
- Define SUPPORTED_LOCALES = ['en', 'es', 'pt'] as const
- Export type Locale, isValidLocale helper, LOCALE_COOKIE constant ('konstruct_locale')
- getRequestConfig reads locale from cookie (LOCALE_COOKIE), falls back to 'en'
- Dynamic import of messages/{locale}.json
- Follow Pattern 1 from RESEARCH.md exactly
3. Update next.config.ts:
- Import createNextIntlPlugin from 'next-intl/plugin'
- Wrap config: const withNextIntl = createNextIntlPlugin('./i18n/request.ts')
- export default withNextIntl({ output: 'standalone' })
4. Create complete messages/en.json with ALL translation keys for every page and component:
- nav: dashboard, employees, chat, usage, billing, apiKeys, users, platform, signOut
- login: title, subtitle, emailLabel, passwordLabel, submitButton, invalidCredentials, signingIn
- dashboard: title, welcome, agentCount, tenantCount, recentActivity, noActivity
- agents: pageTitle, newEmployee, noAgents, noAgentsDescription, createFirst, active, inactive, editButton, deleteButton, confirmDelete, agent fields (name, role, persona, systemPrompt, modelPreference, tools, escalation)
- agentDesigner: title, fields (jobDescription, statementOfWork, persona, systemPrompt, toolAssignments, escalationRules), saveButton, addTool, addRule, condition, action
- agentNew: title, subtitle, options (template, wizard, advanced), descriptions for each
- templates: title, subtitle, deploy, deploying, deployed, recommended, noTemplates, category labels
- wizard: steps (role, persona, tools, channels, escalation, review), labels for each field, next, back, deploy, deployingAgent
- onboarding: title, steps (connectChannel, configureAgent, testMessage), descriptions, buttons, completion message
- chat: title, newConversation, noConversations, noMessages, typeMessage, send, selectAgent, conversations
- billing: title, currentPlan, subscribe, manage, cancelSubscription, upgrade, invoiceHistory
- usage: title, selectTenant, tokenUsage, costBreakdown, timeRange, day, week, month, noBudget
- apiKeys: title, addKey, provider, keyHint, deleteKey, confirmDelete, noKeys
- users: title, inviteUser, name, email, role, status, actions, pending, accepted, revokeInvite
- adminUsers: title, allUsers, platformUsers
- tenants: title, newTenant, name, slug, plan, actions, editTenant, deleteTenant, confirmDelete, noTenants
- tenantForm: nameLabel, slugLabel, planLabel, createButton, updateButton, creating, updating
- common: loading, error, save, cancel, delete, confirm, search, noResults, retry, back, close
- impersonation: banner text, stopButton
- tenantSwitcher: selectTenant, allTenants, currentTenant
- validation: required, invalidEmail, minLength, maxLength, invalidFormat
- language: switcherLabel, en, es, pt
5. Create messages/es.json — complete Spanish translation of ALL keys from en.json. Use proper Latin American Spanish business terminology. Not literal translations — natural phrasing. Example: "Employees" -> "Empleados", "Dashboard" -> "Panel", "Sign out" -> "Cerrar sesion", "AI Workforce" -> "Fuerza laboral IA".
6. Create messages/pt.json — complete Brazilian Portuguese translation of ALL keys from en.json. Use proper Brazilian Portuguese business terminology. Example: "Employees" -> "Funcionarios", "Dashboard" -> "Painel", "Sign out" -> "Sair", "AI Workforce" -> "Forca de trabalho IA".
7. Update app/layout.tsx:
- Keep as Server Component (NO 'use client')
- Import NextIntlClientProvider from 'next-intl'
- Import getLocale, getMessages from 'next-intl/server'
- Make function async
- Call const locale = await getLocale(); const messages = await getMessages();
- Set html lang={locale} (dynamic, not hardcoded "en")
- Wrap body children with <NextIntlClientProvider messages={messages}>
- Keep all existing font and class logic unchanged
</action>
<verify>
<automated>cd /home/adelorenzo/repos/konstruct/packages/portal && npx next build 2>&1 | tail -20</automated>
</verify>
<done>
- next-intl installed and configured
- i18n/request.ts reads locale from cookie
- next.config.ts wrapped with withNextIntl
- Complete en.json, es.json, pt.json message files with keys for every page/component
- Root layout wraps app with NextIntlClientProvider
- Portal builds successfully
</done>
</task>
<task type="auto">
<name>Task 2: Language switcher component + Auth.js JWT language sync + login page locale detection</name>
<files>
packages/portal/components/language-switcher.tsx
packages/portal/components/nav.tsx
packages/portal/lib/auth.ts
packages/portal/lib/auth-types.ts
packages/portal/components/session-sync.tsx
packages/portal/app/(auth)/login/page.tsx
</files>
<action>
1. Create components/language-switcher.tsx:
- 'use client' component
- Three clickable buttons: EN / ES / PT (compact, inline)
- Current locale highlighted (derived from cookie or session)
- On click: (a) set document.cookie with konstruct_locale={locale}, path=/, max-age=31536000; (b) if authenticated, PATCH /api/portal/users/me/language with the locale; (c) call update({ language: locale }) on Auth.js session; (d) router.refresh() to re-render with new locale
- Style: compact row of 3 buttons, fits in sidebar user section. Use sidebar color tokens. Active locale has subtle highlight.
- Accept optional `isPreAuth` prop — when true, skip the DB PATCH and session update (for login page)
2. Add LanguageSwitcher to nav.tsx:
- Import and render <LanguageSwitcher /> between the user info section and the sign-out button
- Keep existing nav structure and styling intact
3. Update Auth.js config in lib/auth.ts:
- In the JWT callback: read user.language from the verify endpoint response and add to token
- Handle trigger="update" case for language: if (trigger === "update" && session?.language) token.language = session.language
- In the session callback: expose token.language on session.user.language
- Follow the exact same pattern already used for active_tenant_id
4. Update auth-types.ts if needed to include language in the session/token types.
5. Update session-sync.tsx:
- After login, sync the locale cookie from session.user.language (if the cookie differs from the session value, update the cookie so i18n/request.ts reads the DB-authoritative value)
6. Update login page (app/(auth)/login/page.tsx):
- On mount (useEffect), detect browser locale via navigator.language.slice(0, 2), check if supported ['en', 'es', 'pt'], set konstruct_locale cookie if no cookie exists yet
- Add <LanguageSwitcher isPreAuth /> near the form (below the sign-in button or in the page header)
- Use useTranslations('login') for all login form strings (title, labels, button, error message)
</action>
<verify>
<automated>cd /home/adelorenzo/repos/konstruct/packages/portal && npx next build 2>&1 | tail -20</automated>
</verify>
<done>
- LanguageSwitcher component renders EN/ES/PT buttons with active highlight
- Sidebar shows language switcher near user avatar
- Changing language updates cookie + DB + JWT + triggers re-render
- Login page detects browser locale and shows language switcher
- Login form strings use useTranslations('login')
- Portal builds successfully
</done>
</task>
</tasks>
<verification>
- Portal builds: `cd packages/portal && npx next build`
- next-intl configured: i18n/request.ts exists, next.config.ts uses withNextIntl
- Message files exist: en.json, es.json, pt.json all have matching key structures
- LanguageSwitcher component exists and is rendered in Nav
- Login page uses useTranslations
</verification>
<success_criteria>
- next-intl v4 installed and configured without URL-based routing
- Complete en/es/pt message files covering all pages and components
- Language switcher in sidebar (post-auth) and login page (pre-auth)
- Language preference persists via cookie + DB + JWT
- Browser locale auto-detected on first visit
- Portal builds without errors
</success_criteria>
<output>
After completion, create `.planning/phases/07-multilanguage/07-02-SUMMARY.md`
</output>

View File

@@ -0,0 +1,294 @@
---
phase: 07-multilanguage
plan: 03
type: execute
wave: 2
depends_on:
- "07-01"
- "07-02"
files_modified:
- packages/portal/components/nav.tsx
- packages/portal/components/agent-designer.tsx
- packages/portal/components/billing-status.tsx
- packages/portal/components/budget-alert-badge.tsx
- packages/portal/components/chat-message.tsx
- packages/portal/components/chat-sidebar.tsx
- packages/portal/components/chat-window.tsx
- packages/portal/components/employee-wizard.tsx
- packages/portal/components/impersonation-banner.tsx
- packages/portal/components/message-volume-chart.tsx
- packages/portal/components/onboarding-stepper.tsx
- packages/portal/components/provider-cost-chart.tsx
- packages/portal/components/subscription-card.tsx
- packages/portal/components/template-gallery.tsx
- packages/portal/components/tenant-form.tsx
- packages/portal/components/tenant-switcher.tsx
- packages/portal/app/(dashboard)/dashboard/page.tsx
- packages/portal/app/(dashboard)/agents/page.tsx
- packages/portal/app/(dashboard)/agents/[id]/page.tsx
- packages/portal/app/(dashboard)/agents/new/page.tsx
- packages/portal/app/(dashboard)/agents/new/templates/page.tsx
- packages/portal/app/(dashboard)/agents/new/wizard/page.tsx
- packages/portal/app/(dashboard)/agents/new/advanced/page.tsx
- packages/portal/app/(dashboard)/chat/page.tsx
- packages/portal/app/(dashboard)/billing/page.tsx
- packages/portal/app/(dashboard)/usage/page.tsx
- packages/portal/app/(dashboard)/usage/[tenantId]/page.tsx
- packages/portal/app/(dashboard)/settings/api-keys/page.tsx
- packages/portal/app/(dashboard)/users/page.tsx
- packages/portal/app/(dashboard)/admin/users/page.tsx
- packages/portal/app/(dashboard)/tenants/page.tsx
- packages/portal/app/(dashboard)/tenants/new/page.tsx
- packages/portal/app/(dashboard)/tenants/[id]/page.tsx
- packages/portal/app/(dashboard)/onboarding/page.tsx
- packages/portal/app/(dashboard)/onboarding/steps/connect-channel.tsx
- packages/portal/app/(dashboard)/onboarding/steps/configure-agent.tsx
- packages/portal/app/(dashboard)/onboarding/steps/test-message.tsx
- packages/portal/app/invite/[token]/page.tsx
- packages/portal/components/wizard-steps/step-role.tsx
- packages/portal/components/wizard-steps/step-persona.tsx
- packages/portal/components/wizard-steps/step-tools.tsx
- packages/portal/components/wizard-steps/step-channels.tsx
- packages/portal/components/wizard-steps/step-escalation.tsx
- packages/portal/components/wizard-steps/step-review.tsx
autonomous: true
requirements:
- I18N-01
- I18N-04
- I18N-05
must_haves:
truths:
- "Every user-visible string in the portal uses useTranslations() instead of hardcoded English"
- "Navigation labels render in the selected language"
- "Agent designer, wizard, and template gallery are fully translated"
- "Onboarding flow steps are fully translated"
- "Error messages and validation text render in the selected language"
- "Chat UI, billing, usage, and all other pages are translated"
artifacts:
- path: "packages/portal/components/nav.tsx"
provides: "Translated navigation labels"
contains: "useTranslations"
- path: "packages/portal/components/employee-wizard.tsx"
provides: "Translated wizard UI"
contains: "useTranslations"
- path: "packages/portal/components/template-gallery.tsx"
provides: "Translated template cards with locale-aware API calls"
contains: "useTranslations"
- path: "packages/portal/app/(dashboard)/chat/page.tsx"
provides: "Translated chat interface"
contains: "useTranslations"
key_links:
- from: "All portal components"
to: "packages/portal/messages/{locale}.json"
via: "useTranslations() hook reading from NextIntlClientProvider context"
pattern: "useTranslations"
- from: "packages/portal/components/template-gallery.tsx"
to: "/api/portal/templates?locale="
via: "Locale query param passed to templates API"
pattern: "locale"
---
<objective>
Extract all hardcoded English strings from every portal page and component, replacing them with `useTranslations()` calls that read from the en/es/pt message files created in Plan 02.
Purpose: This is the core localization work. Every user-visible string in every TSX file must be replaced with a `t('key')` call. Without this, the message files and i18n infrastructure from Plan 02 have no effect.
Output: All 40+ portal TSX files updated with useTranslations() calls. Zero hardcoded English strings remain in user-visible UI.
</objective>
<execution_context>
@/home/adelorenzo/.claude/get-shit-done/workflows/execute-plan.md
@/home/adelorenzo/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/07-multilanguage/07-CONTEXT.md
@.planning/phases/07-multilanguage/07-RESEARCH.md
@.planning/phases/07-multilanguage/07-01-SUMMARY.md
@.planning/phases/07-multilanguage/07-02-SUMMARY.md
<interfaces>
<!-- next-intl usage pattern (from Plan 02) -->
```typescript
// In Client Components ('use client'):
import { useTranslations } from 'next-intl';
const t = useTranslations('namespace');
// Then: <h1>{t('title')}</h1>
// In Server Components:
import { useTranslations } from 'next-intl';
const t = useTranslations('namespace');
// Same API — works in both
// Message file structure (from Plan 02):
// messages/en.json has nested keys: nav.dashboard, agents.pageTitle, etc.
// useTranslations('nav') gives t('dashboard') -> "Dashboard"
```
<!-- Template gallery needs locale-aware API call -->
```typescript
// Plan 01 adds ?locale= param to templates API
// Template gallery must pass current locale when fetching:
// GET /api/portal/templates?locale=es
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Extract strings from all components (nav, sidebar, forms, wizards, chat)</name>
<files>
packages/portal/components/nav.tsx
packages/portal/components/agent-designer.tsx
packages/portal/components/billing-status.tsx
packages/portal/components/budget-alert-badge.tsx
packages/portal/components/chat-message.tsx
packages/portal/components/chat-sidebar.tsx
packages/portal/components/chat-window.tsx
packages/portal/components/employee-wizard.tsx
packages/portal/components/impersonation-banner.tsx
packages/portal/components/message-volume-chart.tsx
packages/portal/components/onboarding-stepper.tsx
packages/portal/components/provider-cost-chart.tsx
packages/portal/components/subscription-card.tsx
packages/portal/components/template-gallery.tsx
packages/portal/components/tenant-form.tsx
packages/portal/components/tenant-switcher.tsx
packages/portal/components/wizard-steps/step-role.tsx
packages/portal/components/wizard-steps/step-persona.tsx
packages/portal/components/wizard-steps/step-tools.tsx
packages/portal/components/wizard-steps/step-channels.tsx
packages/portal/components/wizard-steps/step-escalation.tsx
packages/portal/components/wizard-steps/step-review.tsx
</files>
<action>
For EVERY component listed, apply this transformation:
1. Add `import { useTranslations } from 'next-intl';` (for 'use client' components)
2. At the top of the component function, add `const t = useTranslations('namespace');` where namespace matches the message file key group (e.g., 'nav' for nav.tsx, 'wizard' for wizard steps, 'chat' for chat components)
3. Replace every hardcoded English string with `t('keyName')` — use the exact keys from the en.json message file created in Plan 02
4. For strings with interpolation (e.g., "Welcome, {name}"), use `t('welcome', { name })` and ensure the message file uses ICU format: "Welcome, {name}"
5. For nav.tsx specifically: replace the hardcoded label strings in the navItems array with t() calls. Since navItems is defined outside the component, move the labels inside the component function or use a computed items pattern.
Specific component notes:
- nav.tsx: navItems labels ("Dashboard", "Employees", etc.) -> t('dashboard'), t('employees'), etc. "Sign out" -> t('signOut')
- template-gallery.tsx: Pass locale to templates API call: fetch(`/api/portal/templates?locale=${currentLocale}`). Get current locale from cookie or useLocale() from next-intl.
- employee-wizard.tsx: All step labels, button text, form labels
- onboarding-stepper.tsx: Step titles and descriptions
- agent-designer.tsx: Field labels, button text, placeholders
- chat-window.tsx: "Type a message", "Send", placeholder text
- chat-sidebar.tsx: "New Conversation", "No conversations"
- billing-status.tsx: Status labels, button text
- subscription-card.tsx: Plan names, subscribe/manage buttons
- tenant-form.tsx: Form labels, submit buttons
- tenant-switcher.tsx: "Select tenant", "All tenants"
- impersonation-banner.tsx: Banner text, stop button
- budget-alert-badge.tsx: "No limit set", budget alert text
Do NOT translate:
- Component prop names or internal variable names
- CSS class strings
- API endpoint URLs
- Console.log messages
- aria-label values that are already descriptive (but DO translate user-visible aria-labels)
</action>
<verify>
<automated>cd /home/adelorenzo/repos/konstruct/packages/portal && npx next build 2>&1 | tail -20</automated>
</verify>
<done>
- All 22 component files use useTranslations() for every user-visible string
- No hardcoded English strings remain in component files (except technical strings like URLs, class names)
- Template gallery passes locale to API
- Portal builds successfully
</done>
</task>
<task type="auto">
<name>Task 2: Extract strings from all page files (dashboard, agents, chat, billing, usage, etc.)</name>
<files>
packages/portal/app/(dashboard)/dashboard/page.tsx
packages/portal/app/(dashboard)/agents/page.tsx
packages/portal/app/(dashboard)/agents/[id]/page.tsx
packages/portal/app/(dashboard)/agents/new/page.tsx
packages/portal/app/(dashboard)/agents/new/templates/page.tsx
packages/portal/app/(dashboard)/agents/new/wizard/page.tsx
packages/portal/app/(dashboard)/agents/new/advanced/page.tsx
packages/portal/app/(dashboard)/chat/page.tsx
packages/portal/app/(dashboard)/billing/page.tsx
packages/portal/app/(dashboard)/usage/page.tsx
packages/portal/app/(dashboard)/usage/[tenantId]/page.tsx
packages/portal/app/(dashboard)/settings/api-keys/page.tsx
packages/portal/app/(dashboard)/users/page.tsx
packages/portal/app/(dashboard)/admin/users/page.tsx
packages/portal/app/(dashboard)/tenants/page.tsx
packages/portal/app/(dashboard)/tenants/new/page.tsx
packages/portal/app/(dashboard)/tenants/[id]/page.tsx
packages/portal/app/(dashboard)/onboarding/page.tsx
packages/portal/app/(dashboard)/onboarding/steps/connect-channel.tsx
packages/portal/app/(dashboard)/onboarding/steps/configure-agent.tsx
packages/portal/app/(dashboard)/onboarding/steps/test-message.tsx
packages/portal/app/invite/[token]/page.tsx
</files>
<action>
For EVERY page file listed, apply the same transformation pattern as Task 1:
1. Add `import { useTranslations } from 'next-intl';`
2. Add `const t = useTranslations('namespace');` using the appropriate namespace
3. Replace all hardcoded English strings with `t('key')` calls
Page-specific notes:
- dashboard/page.tsx: "Dashboard", "Welcome back", stats labels -> t('dashboard.*')
- agents/page.tsx: "AI Employees", "New Employee", empty state text -> t('agents.*')
- agents/[id]/page.tsx: Agent detail labels, edit/delete buttons -> t('agentDesigner.*')
- agents/new/page.tsx: Three creation options text -> t('agentNew.*')
- agents/new/templates/page.tsx: Template gallery page title -> t('templates.*')
- agents/new/wizard/page.tsx: Wizard page wrapper -> t('wizard.*')
- agents/new/advanced/page.tsx: Advanced mode labels -> t('agentDesigner.*')
- chat/page.tsx: Chat page labels -> t('chat.*')
- billing/page.tsx: Billing page labels, plan info -> t('billing.*')
- usage/page.tsx & usage/[tenantId]/page.tsx: Usage labels, chart titles -> t('usage.*')
- settings/api-keys/page.tsx: API key management labels -> t('apiKeys.*')
- users/page.tsx: User management, invite labels -> t('users.*')
- admin/users/page.tsx: Platform admin user list -> t('adminUsers.*')
- tenants pages: Tenant management labels -> t('tenants.*')
- onboarding pages + steps: All onboarding UI -> t('onboarding.*')
- invite/[token]/page.tsx: Invitation acceptance page -> t('invite.*') (add invite namespace to message files if not already present)
After all string extraction is complete, do a final review of messages/en.json, messages/es.json, and messages/pt.json to ensure every key used by t() exists in all three files. Add any missing keys discovered during extraction.
IMPORTANT: If any page is a Server Component (no 'use client'), useTranslations still works the same way in next-intl v4 — it reads from the server context set up by i18n/request.ts. No change needed.
</action>
<verify>
<automated>cd /home/adelorenzo/repos/konstruct/packages/portal && npx next build 2>&1 | tail -20</automated>
</verify>
<done>
- All 22 page files use useTranslations() for every user-visible string
- No hardcoded English strings remain in any page file
- All translation keys used in t() calls exist in en.json, es.json, and pt.json
- Portal builds successfully with zero errors
</done>
</task>
</tasks>
<verification>
- Portal builds: `cd packages/portal && npx next build`
- Grep for remaining hardcoded strings: search for obvious English strings that should be translated
- All message file keys are consistent across en.json, es.json, pt.json
</verification>
<success_criteria>
- Every user-visible string in the portal uses useTranslations()
- All three message files (en/es/pt) have matching key structures
- Template gallery passes locale to API for translated template content
- Portal builds without errors
- Zero hardcoded English strings remain in user-facing UI
</success_criteria>
<output>
After completion, create `.planning/phases/07-multilanguage/07-03-SUMMARY.md`
</output>

View File

@@ -0,0 +1,138 @@
---
phase: 07-multilanguage
plan: 04
type: execute
wave: 3
depends_on:
- "07-03"
files_modified: []
autonomous: false
requirements:
- I18N-01
- I18N-02
- I18N-03
- I18N-04
- I18N-05
- I18N-06
must_haves:
truths:
- "Portal renders correctly in English, Spanish, and Portuguese"
- "Language switcher works in sidebar and on login page"
- "Language preference persists across sessions"
- "AI Employee responds in the user's language"
- "Agent templates display in the selected language"
- "Error messages and validation text are localized"
artifacts: []
key_links: []
---
<objective>
Human verification of the complete multilanguage implementation across all portal pages and AI Employee behavior.
Purpose: Verify that translations look correct, the language switcher works end-to-end, and AI Employees respond in the correct language. Automated builds confirm code compiles but cannot verify translation quality or visual correctness.
Output: Confirmation that all I18N requirements are met.
</objective>
<execution_context>
@/home/adelorenzo/.claude/get-shit-done/workflows/execute-plan.md
@/home/adelorenzo/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/phases/07-multilanguage/07-CONTEXT.md
@.planning/phases/07-multilanguage/07-01-SUMMARY.md
@.planning/phases/07-multilanguage/07-02-SUMMARY.md
@.planning/phases/07-multilanguage/07-03-SUMMARY.md
</context>
<tasks>
<task type="checkpoint:human-verify" gate="blocking">
<name>Task 1: Verify complete multilanguage implementation</name>
<files></files>
<action>
Present the user with the verification checklist below. No code changes needed — this is a human review checkpoint.
</action>
<verify>
<automated>cd /home/adelorenzo/repos/konstruct/packages/portal && npx next build 2>&1 | tail -5</automated>
</verify>
<done>User confirms all I18N requirements are met by typing "approved"</done>
<what-built>
Complete multilanguage support across the portal and AI Employee behavior:
- All portal pages and components localized in English, Spanish, and Portuguese
- Language switcher in sidebar (post-auth) and login page (pre-auth)
- Language preference persisted to DB and JWT
- Browser locale auto-detection on first visit
- AI Employees respond in the user's language via system prompt instruction
- Agent templates display translated names/descriptions in the selected language
- Invitation emails sent in the inviting admin's language
</what-built>
<how-to-verify>
Start the dev environment and open the portal:
**1. Login page language detection (I18N-02)**
- Open the login page in a fresh browser/incognito
- If your browser is set to Spanish, the form should show Spanish labels
- Click the ES / PT language buttons to verify the form changes language
- Log in
**2. Language switcher persistence (I18N-02)**
- In the sidebar, find the EN / ES / PT switcher near your avatar
- Click "ES" — all navigation labels and page content should switch to Spanish
- Navigate to different pages (Dashboard, Employees, Chat, Billing) — all should be in Spanish
- Click "PT" — verify Portuguese translations appear
- Log out and log back in — verify the language preference persists (should still be Portuguese)
**3. Portal pages in Spanish (I18N-01, I18N-04, I18N-05)**
- Switch to Spanish and visit each major page:
- Dashboard: verify title, stats labels, welcome message
- Employees: verify page title, "New Employee" button, empty state text
- New Employee options: verify template/wizard/advanced descriptions
- Template gallery: verify template names and descriptions are in Spanish
- Employee wizard: verify all 5 step labels and form fields
- Chat: verify sidebar, message input placeholder, conversation labels
- Billing: verify plan names, button labels, status badges
- Usage: verify chart labels, time range options, budget text
- API Keys: verify page title, add/delete labels
- Users: verify invite form labels, role names, status badges
- Onboarding: verify all 3 step titles and descriptions
- Trigger a validation error (e.g., submit an empty form) — verify error message is in Spanish
**4. Portal pages in Portuguese (I18N-01)**
- Switch to Portuguese and spot-check 3-4 pages for correct translations
**5. AI Employee language response (I18N-03)**
- Open the Chat page
- Start a conversation with any agent
- Send a message in Spanish (e.g., "Hola, como puedo ayudarte?")
- Verify the agent responds in Spanish
- Send a message in Portuguese (e.g., "Ola, como posso ajudar?")
- Verify the agent responds in Portuguese
- Send a message in English — verify English response
**6. Extensibility check (I18N-06)**
- Verify that messages/en.json, messages/es.json, and messages/pt.json exist
- Confirm the file structure means adding a 4th language is just a new JSON file
</how-to-verify>
<resume-signal>Type "approved" if all checks pass, or describe any issues found</resume-signal>
</task>
</tasks>
<verification>
Human verification covers all 6 I18N requirements through manual testing in the browser.
</verification>
<success_criteria>
- All 6 verification steps pass
- No untranslated strings visible when using Spanish or Portuguese
- Language preference persists across sessions
- AI Employee responds in the correct language
</success_criteria>
<output>
After completion, create `.planning/phases/07-multilanguage/07-04-SUMMARY.md`
</output>

View File

@@ -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>
## 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
</user_constraints>
<phase_requirements>
## 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 |
</phase_requirements>
---
## 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 <h1>{t('title')}</h1>;
}
// Client Component (has 'use client')
'use client';
import { useTranslations } from 'next-intl';
export function Nav() {
const t = useTranslations('nav');
return <span>{t('employees')}</span>;
}
```
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 (
<html lang={locale} ...>
<body>
<NextIntlClientProvider messages={messages}>
{children}
</NextIntlClientProvider>
</body>
</html>
);
}
```
**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 (
<html lang={locale} className={`${dmSans.variable} ${jetbrainsMono.variable} h-full antialiased`}>
<body className="min-h-full flex flex-col">
<NextIntlClientProvider messages={messages}>
{children}
</NextIntlClientProvider>
</body>
</html>
);
}
```
### 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 15 (all sourced from official docs); MEDIUM for pitfalls 68 (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)

View File

@@ -0,0 +1,82 @@
---
phase: 7
slug: multilanguage
status: draft
nyquist_compliant: false
wave_0_complete: false
created: 2026-03-25
---
# Phase 7 — Validation Strategy
> Per-phase validation contract for feedback sampling during execution.
---
## Test Infrastructure
| Property | Value |
|----------|-------|
| **Framework** | pytest 8.x + pytest-asyncio (existing) |
| **Config file** | `pyproject.toml` (existing) |
| **Quick run command** | `pytest tests/unit -x -q` |
| **Full suite command** | `pytest tests/ -x` |
| **Estimated runtime** | ~30 seconds |
---
## Sampling Rate
- **After every task commit:** Run `pytest tests/unit -x -q`
- **After every plan wave:** Run `pytest tests/ -x`
- **Before `/gsd:verify-work`:** Full suite must be green
- **Max feedback latency:** 30 seconds
---
## Per-Task Verification Map
| Task ID | Plan | Wave | Requirement | Test Type | Automated Command | File Exists | Status |
|---------|------|------|-------------|-----------|-------------------|-------------|--------|
| 07-xx | 01 | 1 | I18N-03 | unit | `pytest tests/unit/test_system_prompt_builder.py -x` | ✅ extend | ⬜ pending |
| 07-xx | 01 | 1 | I18N-02 | integration | `pytest tests/integration/test_language_preference.py -x` | ❌ W0 | ⬜ pending |
| 07-xx | 01 | 1 | I18N-04 | integration | `pytest tests/integration/test_templates_i18n.py -x` | ❌ W0 | ⬜ pending |
| 07-xx | 02 | 2 | I18N-01 | build | `cd packages/portal && npx next build` | ✅ | ⬜ pending |
| 07-xx | 02 | 2 | I18N-02 | build | `cd packages/portal && npx next build` | ✅ | ⬜ pending |
| 07-xx | 02 | 2 | I18N-06 | build | `cd packages/portal && npx next build` | ✅ | ⬜ pending |
*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*
---
## Wave 0 Requirements
- [ ] `tests/unit/test_system_prompt_builder.py` — extend with TestLanguageInstruction class (I18N-03)
- [ ] `tests/integration/test_language_preference.py` — PATCH language endpoint (I18N-02)
- [ ] `tests/integration/test_templates_i18n.py` — locale-aware templates endpoint (I18N-04)
---
## Manual-Only Verifications
| Behavior | Requirement | Why Manual | Test Instructions |
|----------|-------------|------------|-------------------|
| Portal UI renders correctly in Spanish | I18N-01 | Browser rendering required | Switch to es, verify all labels/buttons translated |
| Portal UI renders correctly in Portuguese | I18N-01 | Browser rendering required | Switch to pt, verify all labels/buttons translated |
| Language switcher persists across sessions | I18N-02 | Requires login/logout cycle | Switch to es, log out, log back in, verify es persists |
| Login page language switcher works pre-auth | I18N-02 | UI interaction | On login page, switch to pt, verify form labels change |
| Agent responds in Spanish when messaged in Spanish | I18N-03 | Requires live LLM | Send Spanish message in chat, verify Spanish response |
| Error messages display in selected language | I18N-05 | UI rendering | Trigger validation error in es locale, verify localized |
---
## Validation Sign-Off
- [ ] All tasks have `<automated>` verify or Wave 0 dependencies
- [ ] Sampling continuity: no 3 consecutive tasks without automated verify
- [ ] Wave 0 covers all MISSING references
- [ ] No watch-mode flags
- [ ] Feedback latency < 30s
- [ ] `nyquist_compliant: true` set in frontmatter
**Approval:** pending