Files

32 KiB
Raw Permalink Blame History

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:

npm install next-intl @formatjs/intl-localematcher negotiator
npm install --save-dev @types/negotiator

(Run in packages/portal/)


Architecture Patterns

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.

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

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

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

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

// 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).

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

-- 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).

# 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.

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

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.

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)

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

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

# 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

# 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

# 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)

Secondary (MEDIUM confidence)

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)