# drop-localization-spec

# Drop Localization (i18n) — Architect Specification

**Project:** Drop Fintech Payment App
**Component:** Internationalization (i18n) — NO/EN/SV
**Author:** John (Architect Agent)
**Date:** 2026-02-17
**Status:** Draft for Review

---

## Executive Summary

This specification defines the complete internationalization (i18n) architecture for Drop, enabling support for Norwegian Bokmål (primary/default), English, and Swedish. The system will extract all hardcoded Norwegian strings from the current codebase, implement a type-safe translation framework using next-intl (the de facto standard for Next.js 16 App Router), and establish a sustainable translation workflow.

**Key Decisions:**
- **Framework:** next-intl (App Router native, type-safe, 0 client-side JS for translations)
- **Primary Language:** Norwegian Bokmål (`nb-NO`) — default, required by law for legal documents
- **Additional Languages:** English (`en`), Swedish (`sv`)
- **Routing Strategy:** Subdirectory-based (`/no/`, `/en/`, `/sv/`) with automatic language detection
- **Migration Approach:** Phased rollout (Phase 1: Framework + UI, Phase 2: Email templates, Phase 3: Legal docs)

---

## 1. Framework Selection

### 1.1 Evaluation Criteria

| Library | Next.js 16 App Router | Type Safety | Bundle Size | Maintenance | Verdict |
|---------|----------------------|-------------|-------------|-------------|---------|
| **next-intl** | ✅ Native support | ✅ Full TS support | ~5KB gzipped | ✅ Active (2024-2026) | **RECOMMENDED** |
| react-i18next | ❌ Client-side only | ⚠️ Partial (manual) | ~18KB gzipped | ✅ Active | Not suitable |
| next-international | ✅ App Router support | ✅ Full TS support | ~8KB gzipped | ⚠️ Smaller community | Alternative |
| react-intl | ❌ Requires workarounds | ⚠️ Partial | ~19KB gzipped | ✅ Active | Not suitable |

**Source:** [next-intl vs react-i18next comparison](https://medium.com/@isurusasanga1999/why-i-chose-next-intl-for-internationalization-in-my-next-js-66c9e49dd486), [i18n library comparison](https://npm-compare.com/next-international,react-i18next,react-intl,react-intl-universal)

### 1.2 Why next-intl?

1. **App Router Native:** Built specifically for Next.js 16 App Router with server component support ([next-intl App Router guide](https://next-intl.dev/docs/getting-started/app-router))
2. **Zero Client-Side JS:** Translations preloaded server-side, sent as props to server components
3. **Type Safety:** Auto-completion for message keys, compile-time checks for missing translations
4. **Routing Built-In:** `[locale]` dynamic segment integration out of the box ([routing setup](https://next-intl.dev/docs/routing/setup))
5. **Format Functions:** ICU message syntax, date/time formatting, number formatting per locale
6. **Production-Ready:** Used by Node.js official website, Sitecore SDK, Vercel templates

**Reference:** [Official next-intl documentation](https://next-intl.dev/docs), [Next.js 16 i18n guide](https://i18nexus.com/tutorials/nextjs/next-intl)

---

## 2. Language Support Matrix

| Locale Code | Language | Variant | Priority | Default | Legal Required | Notes |
|-------------|----------|---------|----------|---------|----------------|-------|
| `nb-NO` | Norwegian | Bokmål | **P0** | ✅ Yes | ✅ Yes | 85-90% of Norwegian population uses Bokmål ([source](https://www.simultrans.com/blog/norwegian-localization-bokmal-or-nynorsk)) |
| `en` | English | Generic | **P1** | ❌ No | ❌ No | International users, diaspora secondary language |
| `sv` | Swedish | Generic | **P2** | ❌ No | ❌ No | Scandinavia expansion (future) |

**Nynorsk Exclusion Rationale:** Drop targets urban areas and general Norwegian population. Bokmål is the standard for fintech/banking in Norway. Nynorsk is primarily rural/western Norway (10% usage) and would require separate legal review. ([source](https://www.simultrans.com/blog/norwegian-localization-bokmal-or-nynorsk))

**Future Expansion:** Arabic, Somali, Polish (after MVP — diaspora remittance corridors)

---

## 3. Current Codebase Analysis

### 3.1 Hardcoded Norwegian Strings Identified

**Total Files with Norwegian Text:** 36 files (from Grep scan)

**Categories:**

| Category | Example Strings | File Count | Complexity |
|----------|----------------|------------|------------|
| **UI Labels** | "Hjem", "Kontoer", "Historikk", "Profil", "Send", "Skann" | ~15 | Low |
| **Form Validation** | "E-post og passord er påkrevd", "Ugyldig e-postadresse" | ~8 | Low |
| **Dashboard Content** | "God morgen", "God ettermiddag", "God kveld", "Brukskonto" | ~5 | Medium |
| **Email Templates** | "Velkommen til Drop", "Verifiser konto", "Send penger internasjonalt" | 3 | High |
| **Legal Documents** | "Vilkår for bruk", "Om tjenesten", "Krav til brukere" | 3 | High |
| **API Error Messages** | "Too many requests", "Invalid credentials", "Email and password required" | ~10 | Medium |
| **Notifications** | "Vipps-innlogging kommer snart!", "Oppdatert via BankID" | ~5 | Low |

**Files Requiring Extraction (High Priority):**

1. **UI Components:**
   - `src/components/bottom-nav.tsx` — Navigation labels (Hjem, Kontoer, Historikk, Profil)
   - `src/app/dashboard/page.tsx` — Greetings, account labels
   - `src/app/login/page.tsx` — Form labels, validation errors, button text
   - `src/app/register/page.tsx` — Registration flow text
   - `src/app/send/page.tsx` — Remittance form
   - `src/app/scan/page.tsx` — QR payment UI

2. **API Routes:**
   - `src/app/api/auth/login/route.ts` — "Invalid credentials", "Email and password required"
   - `src/app/api/transactions/*/route.ts` — Transaction error messages

3. **Email Templates:**
   - `src/email-templates/welcome.html` — Full Norwegian welcome email
   - `src/email-templates/transaction-receipt.html` — Receipt email
   - `src/email-templates/password-reset.html` — Password reset email

4. **Legal Pages:**
   - `src/app/terms/page.tsx` — Full terms of service (Norwegian)
   - `src/app/privacy/page.tsx` — Privacy policy
   - `src/app/fees/page.tsx` — Fee schedule

### 3.2 Special Cases

**Currency Formatting:**
- Current: `user.totalBalance.toLocaleString("nb-NO", { minimumFractionDigits: 0 })` (hardcoded locale)
- New: Use next-intl's `useFormatter()` hook for locale-aware formatting

**Date Formatting:**
- Current: No date formatting found (transactions show ISO strings in tests)
- New: Implement `formatDateTime()` from next-intl

**Number Formatting:**
- Current: Hardcoded "kr" currency symbol, hardcoded "NOK" suffix
- New: `formatNumber(value, {style: 'currency', currency: 'NOK'})`

---

## 4. Translation File Structure

### 4.1 Directory Layout

```
src/drop-app/
├── messages/                  # Translation files (JSON)
│   ├── nb-NO.json            # Norwegian Bokmål (default)
│   ├── en.json               # English
│   ├── sv.json               # Swedish
│   └── README.md             # Translation guidelines
├── i18n/                     # i18n configuration
│   ├── config.ts             # Locale definitions, default locale
│   └── request.ts            # next-intl request config (App Router)
├── middleware.ts             # Locale detection middleware
└── app/
    └── [locale]/             # Locale-based routing
        ├── layout.tsx        # Root layout with NextIntlClientProvider
        ├── page.tsx          # Redirects to /dashboard
        ├── dashboard/
        ├── login/
        └── ...               # All existing routes nested under [locale]
```

### 4.2 Namespace Strategy

**Single JSON file per locale (initial approach):**
For Drop MVP, all translations in one file per locale. Future: split into namespaces as app grows.

**File:** `messages/nb-NO.json`

```json
{
  "common": {
    "app_name": "Drop",
    "tagline": "Enklere betalinger. Lavere gebyrer.",
    "loading": "Laster...",
    "error": "Noe gikk galt",
    "retry": "Prøv igjen",
    "cancel": "Avbryt",
    "confirm": "Bekreft",
    "save": "Lagre"
  },
  "nav": {
    "home": "Hjem",
    "accounts": "Kontoer",
    "scan": "Skann",
    "transactions": "Historikk",
    "profile": "Profil"
  },
  "login": {
    "title": "Logg inn",
    "email_label": "E-post",
    "password_label": "Passord",
    "submit": "Logg inn",
    "error_required": "E-post og passord er påkrevd",
    "error_invalid_email": "Ugyldig e-postadresse",
    "error_invalid_credentials": "Feil e-post eller passord",
    "bankid_button": "BankID",
    "vipps_button": "Vipps",
    "vipps_coming_soon": "Vipps-innlogging kommer snart!"
  },
  "dashboard": {
    "greeting_morning": "God morgen",
    "greeting_afternoon": "God ettermiddag",
    "greeting_evening": "God kveld",
    "account_label": "{bankName} Brukskonto",
    "account_updated": "Oppdatert via BankID",
    "action_send": "Send",
    "action_scan": "Skann",
    "action_accounts": "Kontoer",
    "action_history": "Historikk"
  },
  "validation": {
    "required": "Dette feltet er påkrevd",
    "invalid_email": "Ugyldig e-postadresse",
    "invalid_phone": "Ugyldig telefonnummer",
    "min_age": "Du må være minst 18 år",
    "invalid_amount": "Ugyldig beløp"
  },
  "email": {
    "welcome_subject": "Velkommen til Drop!",
    "welcome_body": "Vi er glade for å ha deg med. Med Drop kan du sende penger internasjonalt og betale i butikk – enklere og billigere enn noen gang.",
    "verify_cta": "Verifiser konto",
    "support_email": "support@getdrop.no"
  },
  "legal": {
    "terms_title": "Vilkår for bruk",
    "privacy_title": "Personvernerklæring",
    "fees_title": "Gebyrer og priser"
  },
  "errors": {
    "rate_limited": "For mange forsøk. Prøv igjen senere.",
    "unauthorized": "Du må logge inn for å fortsette",
    "not_found": "Siden finnes ikke",
    "server_error": "Noe gikk galt. Prøv igjen senere."
  }
}
```

**File:** `messages/en.json`

```json
{
  "common": {
    "app_name": "Drop",
    "tagline": "Easier payments. Lower fees.",
    "loading": "Loading...",
    "error": "Something went wrong",
    "retry": "Try again",
    "cancel": "Cancel",
    "confirm": "Confirm",
    "save": "Save"
  },
  "nav": {
    "home": "Home",
    "accounts": "Accounts",
    "scan": "Scan",
    "transactions": "History",
    "profile": "Profile"
  },
  "login": {
    "title": "Log in",
    "email_label": "Email",
    "password_label": "Password",
    "submit": "Log in",
    "error_required": "Email and password required",
    "error_invalid_email": "Invalid email address",
    "error_invalid_credentials": "Invalid email or password",
    "bankid_button": "BankID",
    "vipps_button": "Vipps",
    "vipps_coming_soon": "Vipps login coming soon!"
  }
  // ... rest of translations
}
```

---

## 5. Key Translation Categories

### 5.1 UI Text (Priority 1)

**Scope:** All visible text in React components, buttons, labels, placeholders, tooltips.

**Extraction Method:**
1. Search for JSX text content: `<span>Text</span>` → `<span>{t('key')}</span>`
2. Search for string literals in className, title, aria-label
3. Replace hardcoded strings with `t()` calls

**Tools:** Manual extraction + ESLint rule to prevent future hardcoded strings

**Example (Before):**
```tsx
<span className="text-xs text-[#1E293B]">Hjem</span>
```

**Example (After):**
```tsx
<span className="text-xs text-[#1E293B]">{t('nav.home')}</span>
```

### 5.2 Error Messages (Priority 1)

**Scope:** API route error responses, form validation errors, toast notifications.

**Current State:** Mix of English and Norwegian error messages in API routes.

**Strategy:**
- Backend API routes return error **keys** (not localized strings)
- Frontend translates error keys using next-intl
- Fallback to English for unknown keys

**Example (API Route — Before):**
```typescript
return jsonError("unauthorized", "Invalid credentials", 401);
```

**Example (API Route — After):**
```typescript
return jsonError("unauthorized", "errors.invalid_credentials", 401);
// Note: Second param is translation KEY, not message
```

**Example (Frontend):**
```tsx
const errorMessage = t(`errors.${errorKey}`);
toast.error(errorMessage);
```

### 5.3 Email Templates (Priority 2)

**Scope:** 3 email templates (welcome.html, transaction-receipt.html, password-reset.html)

**Challenge:** HTML email templates don't support React components.

**Solution:**
1. Create **template functions** that accept locale parameter
2. Store email translations in same `messages/*.json` files under `email.*` namespace
3. Server-side template rendering with locale-specific strings

**Current:** `src/email-templates/welcome.html` (static Norwegian HTML)

**New:** `src/lib/email-templates.ts`

```typescript
import { getTranslations } from 'next-intl/server';

export async function renderWelcomeEmail(locale: string, data: {verifyUrl: string}) {
  const t = await getTranslations({locale, namespace: 'email'});

  return `
    <!DOCTYPE html>
    <html lang="${locale}">
    <head><title>${t('welcome_subject')}</title></head>
    <body>
      <h1>${t('welcome_subject')}</h1>
      <p>${t('welcome_body')}</p>
      <a href="${data.verifyUrl}">${t('verify_cta')}</a>
    </body>
    </html>
  `;
}
```

**Email Sending:**
```typescript
const html = await renderWelcomeEmail(user.preferredLanguage || 'nb-NO', {verifyUrl});
await sendEmail({to: user.email, subject: t('email.welcome_subject'), html});
```

### 5.4 Legal Documents (Priority 3 — Manual Translation Required)

**Scope:** Terms of Service, Privacy Policy, Fee Schedule

**Legal Requirement:** Norwegian version MUST exist and be primary ([PSD2 Norway implementation](https://www.lexology.com/library/detail.aspx?g=26ea2eec-8839-48b9-b43a-769444290aa1)). English/Swedish versions are optional.

**Strategy:**
1. **Phase 1 (MVP):** Norwegian-only legal docs (current state)
2. **Phase 2 (Post-MVP):** Professional translation of legal docs to English (external translator)
3. Store legal content as **Markdown files** in `messages/legal/[locale]/` directory
4. Render Markdown server-side using `@next/mdx` or similar

**Structure:**
```
messages/
└── legal/
    ├── nb-NO/
    │   ├── terms.md
    │   ├── privacy.md
    │   └── fees.md
    ├── en/
    │   ├── terms.md
    │   ├── privacy.md
    │   └── fees.md
    └── sv/
        └── ...
```

**Legal Page Component:**
```tsx
import fs from 'fs/promises';
import { compileMDX } from 'next-mdx-remote/rsc';

export default async function TermsPage({params}: {params: {locale: string}}) {
  const locale = params.locale || 'nb-NO';
  const source = await fs.readFile(`messages/legal/${locale}/terms.md`, 'utf8');
  const {content} = await compileMDX({source});
  return <div className="prose">{content}</div>;
}
```

**Fallback:** If English/Swedish legal docs don't exist, show Norwegian version with disclaimer: "Legal documents available in Norwegian only."

---

## 6. Formatting Standards

### 6.1 Currency Formatting

**Norwegian Locale (nb-NO):**
- Format: `1 234,56 kr` (space as thousand separator, comma as decimal, suffix "kr")
- Alternative (banking): `NOK 1 234,56` (ISO code prefix)
- **Drop Standard:** Use `1 234 kr` (no decimals for whole amounts) to match UX mockups

**English Locale (en):**
- Format: `NOK 1,234.56` (ISO code, comma thousand separator, period decimal)

**Swedish Locale (sv):**
- Format: `1 234,56 kr` (same as Norwegian)

**Implementation (next-intl):**
```tsx
import {useFormatter} from 'next-intl';

const format = useFormatter();
const formatted = format.number(1234.56, {
  style: 'currency',
  currency: 'NOK',
  minimumFractionDigits: 0, // Drop shows whole kroner
  maximumFractionDigits: 0
});
// nb-NO: "1 235 kr"
// en: "NOK 1,235"
```

**Reference:** [Microsoft Currency Formatting Guide](https://learn.microsoft.com/en-us/globalization/locale/currency-formats), [Norwegian Bokmål locale formatting](https://leap.hcldoc.com/help/topic/SSS28S_8.2.1/XFDL_Specification/i_xfdl_r_formats_nb_NO.html)

### 6.2 Date & Time Formatting

**Norwegian (nb-NO):**
- Short date: `17.02.2026` (DD.MM.YYYY)
- Long date: `17. februar 2026`
- Time: `14:30` (24-hour clock)

**English (en):**
- Short date: `02/17/2026` (MM/DD/YYYY) or `17/02/2026` (international)
- Long date: `February 17, 2026`
- Time: `2:30 PM` (12-hour clock)

**Implementation:**
```tsx
const format = useFormatter();
const date = new Date('2026-02-17T14:30:00Z');

format.dateTime(date, {dateStyle: 'short'});
// nb-NO: "17.02.2026"
// en: "2/17/2026"

format.dateTime(date, {dateStyle: 'long', timeStyle: 'short'});
// nb-NO: "17. februar 2026 kl. 14:30"
// en: "February 17, 2026 at 2:30 PM"
```

### 6.3 Number Formatting

**Norwegian (nb-NO):**
- Decimal separator: `,` (comma)
- Thousand separator: ` ` (non-breaking space)
- Example: `1 234 567,89`

**English (en):**
- Decimal separator: `.` (period)
- Thousand separator: `,` (comma)
- Example: `1,234,567.89`

**Implementation:**
```tsx
format.number(1234567.89, {maximumFractionDigits: 2});
// nb-NO: "1 234 567,89"
// en: "1,234,567.89"
```

---

## 7. Translation Workflow

### 7.1 Roles & Responsibilities

| Role | Responsibility | Tools |
|------|---------------|-------|
| **Developer** | Extract strings to translation files, add translation keys to code | VSCode, ESLint |
| **Content Lead (Alem)** | Review Norwegian translations for accuracy, approve final content | GitHub PR review |
| **External Translator** | Translate `nb-NO.json` → `en.json`, `sv.json` (Phase 2) | JSON editor, CAT tool |
| **Legal Team** | Translate legal documents (terms, privacy, fees) | Markdown editor |
| **QA** | Test all locales, verify formatting, check for missing translations | Browser, Playwright |

### 7.2 Translation Process

**Phase 1: Initial Extraction (Developer)**

1. Create `messages/nb-NO.json` from existing Norwegian strings
2. Structure translation keys by namespace (`common`, `nav`, `login`, etc.)
3. Replace hardcoded strings in components with `t('key')` calls
4. Run build to verify no missing keys (TypeScript will catch errors)
5. Test Norwegian locale thoroughly (should match current behavior exactly)

**Phase 2: English Translation (External Translator)**

1. Export `messages/nb-NO.json`
2. Translator creates `messages/en.json` (JSON structure preserved, values translated)
3. Developer imports `en.json`, runs build
4. QA tests English locale

**Phase 3: Swedish Translation (Future)**

Same process as Phase 2.

### 7.3 Quality Assurance

**Pre-Deployment Checklist:**

- [ ] All UI screens tested in all 3 locales (nb-NO, en, sv)
- [ ] Currency formatting correct for each locale
- [ ] Date formatting correct for each locale
- [ ] No missing translation keys (TypeScript build passes)
- [ ] Email templates render correctly in all locales
- [ ] Legal documents exist for required locales (nb-NO mandatory)
- [ ] Language switcher works (Profile → Language)
- [ ] URL routing works (`/no/dashboard`, `/en/dashboard`, `/sv/dashboard`)
- [ ] Fallback to Norwegian works if user selects unsupported locale
- [ ] Browser language detection works (first visit)

**Automated Tests:**

1. **Playwright E2E:** Test key user flows in all 3 locales
2. **Unit Tests:** Test `formatCurrency()`, `formatDate()` with mock locales
3. **Snapshot Tests:** Compare rendered output in different locales

---

## 8. Database Content Localization

### 8.1 User-Generated Content

**Scope:** User names, recipient names, custom notes on transactions.

**Strategy:** **NOT translated.** User-generated content stored as-is in database.

### 8.2 System-Generated Content

**Scope:** Transaction status messages, notification content, system emails.

**Strategy:**
- Store **translation keys** in database, not localized text
- Render localized text at display time based on user's preferred language

**Example:**

**Database:**
```sql
INSERT INTO notifications (user_id, message_key, data_json) VALUES
(123, 'notification.transfer_completed', '{"amount": 500, "recipient": "John Doe"}');
```

**Display (React component):**
```tsx
const t = useTranslations('notification');
const notification = await getNotification(id);
const message = t(notification.message_key, JSON.parse(notification.data_json));
// nb-NO: "Overføring på 500 kr til John Doe er fullført"
// en: "Transfer of NOK 500 to John Doe completed"
```

**Translation File:**
```json
{
  "notification": {
    "transfer_completed": "{amount, number, ::currency/NOK} til {recipient} er fullført"
  }
}
```

### 8.3 Static Reference Data

**Scope:** Country names, bank names, currency names.

**Strategy:**
- Store in separate `reference-data` namespace
- Pre-translate all reference data (drop countries, supported currencies)

**Example:**
```json
{
  "countries": {
    "NO": "Norge",
    "SE": "Sverige",
    "PL": "Polen"
  },
  "currencies": {
    "NOK": "Norske kroner",
    "SEK": "Svenske kroner",
    "EUR": "Euro"
  }
}
```

---

## 9. URL & Routing Strategy

### 9.1 Subdirectory-Based Routing (Recommended)

**Structure:** `/[locale]/[route]`

**Examples:**
- `/no/dashboard` — Norwegian
- `/en/dashboard` — English
- `/sv/dashboard` — Swedish
- `/` — Root, redirects to `/no/` (default)

**Pros:**
- SEO-friendly (separate URL per language)
- Shareable links preserve language
- Server-side rendering compatible
- No cookie/session required for language persistence

**Cons:**
- URL changes when switching language (acceptable trade-off)

**Implementation:**

**File:** `src/middleware.ts`
```typescript
import createMiddleware from 'next-intl/middleware';

export default createMiddleware({
  locales: ['nb-NO', 'en', 'sv'],
  defaultLocale: 'nb-NO',
  localePrefix: 'as-needed' // /nb-NO/dashboard → /dashboard (default), /en/dashboard (explicit)
});

export const config = {
  matcher: ['/', '/(nb-NO|en|sv)/:path*']
};
```

**File:** `src/app/[locale]/layout.tsx`
```tsx
import {NextIntlClientProvider} from 'next-intl';
import {notFound} from 'next/navigation';

export default async function LocaleLayout({
  children,
  params: {locale}
}: {
  children: React.ReactNode;
  params: {locale: string};
}) {
  let messages;
  try {
    messages = (await import(`@/messages/${locale}.json`)).default;
  } catch (error) {
    notFound();
  }

  return (
    <html lang={locale}>
      <body>
        <NextIntlClientProvider locale={locale} messages={messages}>
          {children}
        </NextIntlClientProvider>
      </body>
    </html>
  );
}
```

### 9.2 Language Detection

**Priority:**
1. URL locale (`/en/dashboard` → `en`)
2. User profile `preferred_language` (if logged in)
3. Browser `Accept-Language` header (first visit)
4. Fallback to `nb-NO` (default)

**Implementation:**

**File:** `src/i18n/config.ts`
```typescript
import {getRequestConfig} from 'next-intl/server';
import {headers} from 'next/headers';

export default getRequestConfig(async ({locale}) => {
  // Locale from URL (/en/dashboard) or middleware detection
  return {
    messages: (await import(`../messages/${locale}.json`)).default
  };
});
```

**User Preference Storage:**
```sql
ALTER TABLE users ADD COLUMN preferred_language TEXT DEFAULT 'nb-NO';
```

**API Route to Update Preference:**
```typescript
// POST /api/settings/language
await db.run('UPDATE users SET preferred_language = ? WHERE id = ?', [locale, userId]);
```

### 9.3 Language Switcher UI

**Location:** Profile → Language Settings (`/[locale]/profile/language`)

**UI:**
```tsx
import {useRouter, usePathname} from 'next/navigation';
import {useLocale} from 'next-intl';

export function LanguageSwitcher() {
  const router = useRouter();
  const pathname = usePathname();
  const currentLocale = useLocale();

  const switchLocale = (newLocale: string) => {
    const newPath = pathname.replace(`/${currentLocale}`, `/${newLocale}`);
    router.push(newPath);
  };

  return (
    <select value={currentLocale} onChange={(e) => switchLocale(e.target.value)}>
      <option value="nb-NO">🇳🇴 Norsk</option>
      <option value="en">🇬🇧 English</option>
      <option value="sv">🇸🇪 Svenska</option>
    </select>
  );
}
```

---

## 10. Legal Requirements (PSD2 / Finanstilsynet)

### 10.1 Mandatory Norwegian Disclosure

**Requirement:** Payment service providers in Norway must comply with **host state rules** on disclosure obligations and customer protection ([PSD2 Norway implementation](https://svw.no/en/insights/norway-finally-implements-psd2)).

**Implication:** Terms of Service, Privacy Policy, and Fee Schedule **MUST** be available in Norwegian.

**Compliance:**
- Norwegian legal documents are **mandatory** (P0)
- English/Swedish legal documents are **optional** (P2, improves UX for non-Norwegian speakers)
- If non-Norwegian user selects English, show English UI but link to Norwegian legal docs with disclaimer:
  > "Legal documents are provided in Norwegian only, as required by Norwegian law. For an unofficial translation, please use a translation service."

### 10.2 Consent & Agreement Language

**Requirement:** User must **consent** in a language they understand.

**Strategy:**
- During onboarding, detect user's language preference
- Display Terms of Service in user's language (if available)
- If not available, show Norwegian with disclaimer + user confirms they understand
- Log consent with `language_of_consent` in database

**Database:**
```sql
ALTER TABLE users ADD COLUMN language_of_consent TEXT;
-- Example: 'nb-NO', 'en', 'sv'
```

### 10.3 Customer Support Language

**Requirement:** Not legally mandated, but best practice.

**Strategy:**
- Support email (`support@getdrop.no`) responds in **Norwegian** (primary) and **English** (secondary)
- Swedish support on-demand (Google Translate fallback initially)

---

## 11. Testing Strategy

### 11.1 Unit Tests

**Framework:** Vitest (already in use)

**Test Files:**
- `src/lib/i18n/formatters.test.ts` — Currency, date, number formatting
- `src/lib/i18n/translations.test.ts` — Translation key coverage

**Example:**
```typescript
import {formatCurrency} from '@/lib/formatters';

describe('formatCurrency', () => {
  it('formats NOK in Norwegian locale', () => {
    expect(formatCurrency(1234, 'nb-NO')).toBe('1 234 kr');
  });

  it('formats NOK in English locale', () => {
    expect(formatCurrency(1234, 'en')).toBe('NOK 1,234');
  });
});
```

### 11.2 Integration Tests

**Framework:** Playwright (already in use)

**Test Scenarios:**
1. **Language Switcher:** Switch from Norwegian → English → Swedish, verify UI updates
2. **Currency Formatting:** Check dashboard balance shows correct format per locale
3. **Date Formatting:** Check transaction history dates render correctly
4. **Email Templates:** Generate email in each locale, verify content
5. **Legal Pages:** Load terms/privacy in each locale, verify fallback if missing

**Example:**
```typescript
test('dashboard shows correct currency format for Norwegian locale', async ({page}) => {
  await page.goto('/nb-NO/dashboard');
  await expect(page.locator('text=/\\d+ kr/')).toBeVisible(); // "1 234 kr" format
});

test('dashboard shows correct currency format for English locale', async ({page}) => {
  await page.goto('/en/dashboard');
  await expect(page.locator('text=/NOK \\d+/')).toBeVisible(); // "NOK 1,234" format
});
```

### 11.3 Manual QA Checklist

**Pre-Release Testing (All Locales):**

| Test Case | nb-NO | en | sv | Notes |
|-----------|-------|----|----|-------|
| Login page renders correctly | ☐ | ☐ | ☐ | Check labels, buttons, errors |
| Dashboard shows greeting in correct language | ☐ | ☐ | ☐ | "God morgen" vs "Good morning" |
| Currency formatting matches locale | ☐ | ☐ | ☐ | "1 234 kr" vs "NOK 1,234" |
| Transaction history dates formatted correctly | ☐ | ☐ | ☐ | DD.MM.YYYY vs MM/DD/YYYY |
| Email templates render in correct language | ☐ | ☐ | ☐ | Send test emails |
| Legal pages load without errors | ☐ | ☐ | ☐ | Check fallback for missing translations |
| Language switcher changes UI language | ☐ | ☐ | ☐ | From profile settings |
| URL routing works for all locales | ☐ | ☐ | ☐ | /nb-NO/, /en/, /sv/ |

---

## 12. Migration Plan (Phased Rollout)

### Phase 1: Framework + Core UI (Week 1-2)

**Goal:** Replace all hardcoded UI strings with next-intl translations. No new languages yet (Norwegian-only, but structured for future).

**Tasks:**

1. **Install next-intl:** `npm install next-intl`
2. **Create translation files:**
   - `messages/nb-NO.json` (copy all existing Norwegian strings)
   - Extract strings from:
     - Navigation (`bottom-nav.tsx`)
     - Login (`login/page.tsx`)
     - Dashboard (`dashboard/page.tsx`)
     - Profile (`profile/page.tsx`)
     - All form validation errors
3. **Setup routing:**
   - Create `app/[locale]/` directory
   - Move all existing routes under `[locale]/`
   - Add middleware for locale detection
4. **Update components:**
   - Replace `"Hjem"` with `t('nav.home')`
   - Replace `toLocaleString("nb-NO")` with `format.number()`
5. **Test:** Verify app works exactly as before (Norwegian-only, but via next-intl)

**Deliverables:**
- `messages/nb-NO.json` (complete)
- All UI components use `useTranslations()` hook
- Zero hardcoded Norwegian strings in TSX files
- Build passes, Playwright tests pass

**Success Criteria:** App looks/behaves identical to current version, but all strings come from translation file.

---

### Phase 2: English + Email Templates (Week 3)

**Goal:** Add English locale, translate email templates.

**Tasks:**

1. **Translate UI to English:**
   - Create `messages/en.json` (external translator or Alem review)
   - Add "English" option to language switcher
2. **Refactor email templates:**
   - Convert `email-templates/*.html` to `lib/email-templates.ts` functions
   - Add `email.*` namespace to translation files
   - Update email sending logic to accept locale parameter
3. **Test emails:**
   - Send test emails in Norwegian and English
   - Verify formatting (date/currency in emails)

**Deliverables:**
- `messages/en.json` (complete)
- Email templates support both locales
- Language switcher functional

**Success Criteria:** Users can switch to English, all UI + emails render correctly in English.

---

### Phase 3: Swedish + Legal Docs (Week 4)

**Goal:** Add Swedish locale, translate legal documents (or defer if not ready).

**Tasks:**

1. **Translate UI to Swedish:**
   - Create `messages/sv.json`
   - Add "Svenska" to language switcher
2. **Legal document strategy:**
   - Option A: Professional translation of terms/privacy/fees to English (defer Swedish)
   - Option B: Keep Norwegian-only legal docs, show disclaimer for EN/SV users
3. **Production deployment:**
   - Deploy with all 3 locales
   - Monitor for missing translations (Sentry alerts)

**Deliverables:**
- `messages/sv.json` (complete)
- Legal document translation strategy finalized
- Production-ready i18n system

**Success Criteria:** All 3 locales functional, legal compliance maintained.

---

### Phase 4: Polish & Optimization (Week 5+)

**Goal:** Refine translations, add missing edge cases, optimize bundle size.

**Tasks:**

1. **Translation review:**
   - Native speakers review Norwegian/Swedish translations
   - Collect user feedback on clarity
2. **Namespace splitting:**
   - Split large `nb-NO.json` into `common.json`, `auth.json`, `dashboard.json`, etc.
   - Lazy-load translation namespaces for faster initial load
3. **ESLint rule:**
   - Add ESLint rule to prevent future hardcoded strings:
     ```javascript
     // .eslintrc.js
     rules: {
       'no-restricted-syntax': [
         'error',
         {
           selector: 'JSXText[value=/[a-zæøåA-ZÆØÅ]{3,}/]',
           message: 'Hardcoded text not allowed. Use useTranslations() hook.'
         }
       ]
     }
     ```
4. **Performance audit:**
   - Measure bundle size impact of next-intl
   - Verify server-side rendering works (translations in initial HTML)

**Success Criteria:** i18n system stable, maintainable, performant.

---

## 13. Implementation Estimates

| Phase | Tasks | Effort | Owner |
|-------|-------|--------|-------|
| **Phase 1: Framework + Core UI** | Install next-intl, extract strings, setup routing, update components | 2-3 days | Builder agent |
| **Phase 2: English + Email Templates** | Translate UI, refactor email templates, test | 1-2 days | Builder + Translator |
| **Phase 3: Swedish + Legal Docs** | Translate UI, legal doc strategy, deploy | 1-2 days | Builder + Legal |
| **Phase 4: Polish & Optimization** | Review, namespace split, ESLint rule, audit | 1-2 days | Builder + Validator |
| **Total** | Full i18n implementation | **5-9 days** | Team |

**Assumptions:**
- Developer familiar with next-intl (1 day learning curve included)
- External translator available for English (1 day turnaround)
- Legal document translation deferred to post-MVP (Phase 3 can proceed with Norwegian-only legal docs)

---

## 14. Risks & Mitigation

| Risk | Impact | Likelihood | Mitigation |
|------|--------|------------|------------|
| **Missing translations break production** | High | Medium | TypeScript type-checking catches missing keys at build time. Add runtime fallback to Norwegian. |
| **Translation quality poor (machine translation)** | Medium | High | Use professional translator for English. Native speaker review for Swedish. |
| **Legal documents not compliant in English** | High | Low | Keep Norwegian legal docs as source of truth. English is "best effort" unofficial translation. |
| **Performance regression (bundle size)** | Low | Low | next-intl adds ~5KB gzipped. Negligible for Drop's use case. |
| **URL routing breaks SEO** | Medium | Low | Subdirectory routing (`/en/`) is SEO-friendly. Submit all locales to Google Search Console. |
| **User confusion with language switcher** | Low | Medium | Add clear UI labels, remember user preference in profile. |
| **Email templates render incorrectly in some locales** | Medium | Medium | Test emails in all locales before production. Use email testing tool (Litmus, Email on Acid). |

---

## 15. Success Metrics

**Post-Deployment (30 days):**

| Metric | Target | Measurement |
|--------|--------|-------------|
| **Locale adoption (English)** | >10% of users | Analytics: % of users with `/en/` routes |
| **Translation coverage** | 100% of UI strings | Automated script: check all keys exist in all locales |
| **Zero missing translation errors** | 0 Sentry errors | Sentry: no `TranslationKeyNotFound` errors |
| **Email deliverability (all locales)** | >95% delivery rate | Email service provider metrics |
| **Legal compliance** | 100% Norwegian terms visible | Manual audit: all users see Norwegian legal docs |

---

## 16. Future Enhancements (Post-MVP)

1. **Right-to-Left (RTL) Support:** Arabic, Somali for diaspora remittance corridors
2. **Translation Management Platform:** Use [Locize](https://locize.com/) or [Crowdin](https://crowdin.com/) for professional translation workflow
3. **A/B Testing:** Test Norwegian vs English CTAs for conversion optimization
4. **Voice of Customer:** Collect user feedback on translation quality, iterate
5. **Automatic Language Detection:** Use IP geolocation to suggest locale (Norway → Norwegian, USA → English)

---

## 17. References

### Documentation
- [next-intl Official Docs](https://next-intl.dev/docs)
- [next-intl App Router Setup](https://next-intl.dev/docs/getting-started/app-router)
- [Next.js 16 i18n Tutorial](https://i18nexus.com/tutorials/nextjs/next-intl)
- [Norwegian Bokmål Locale Formatting](https://leap.hcldoc.com/help/topic/SSS28S_8.2.1/XFDL_Specification/i_xfdl_r_formats_nb_NO.html)

### Legal & Compliance
- [PSD2 Implementation in Norway](https://svw.no/en/insights/norway-finally-implements-psd2)
- [PSD2 Legal Requirements (Lexology)](https://www.lexology.com/library/detail.aspx?g=26ea2eec-8839-48b9-b43a-769444290aa1)
- [Finanstilsynet PSD2 Guidance](https://www.finanstilsynet.no/tema/psd-2---eus-reviderte-betalingstjenestedirektiv/psd2---presiseringer-og-avklaringer-om-regelverket/)

### Localization Best Practices
- [Norwegian Bokmål vs Nynorsk Localization](https://www.simultrans.com/blog/norwegian-localization-bokmal-or-nynorsk)
- [Currency Formatting for Localization](https://www.sitetran.com/blog/localization-for-growth/Currency-Formatting-for-Localization-Success)
- [Microsoft Currency Formatting Guide](https://learn.microsoft.com/en-us/globalization/locale/currency-formats)

### Library Comparisons
- [next-intl vs react-i18next Comparison](https://medium.com/@isurusasanga1999/why-i-chose-next-intl-for-internationalization-in-my-next-js-66c9e49dd486)
- [i18n Libraries Comparison](https://npm-compare.com/next-international,react-i18next,react-intl,react-intl-universal)

---

## 18. Appendix: Translation File Examples

### A. Complete nb-NO.json (Sample)

See **Section 4.2** for full structure.

### B. ESLint Rule for Hardcoded Strings

```javascript
// .eslintrc.js
module.exports = {
  rules: {
    'no-restricted-syntax': [
      'error',
      {
        selector: 'JSXText[value=/[a-zæøåA-ZÆØÅ]{3,}/]',
        message: 'Hardcoded text in JSX not allowed. Use useTranslations() hook from next-intl.'
      }
    ]
  }
};
```

### C. next-intl Configuration Files

**File:** `src/i18n/config.ts`
```typescript
export const locales = ['nb-NO', 'en', 'sv'] as const;
export type Locale = typeof locales[number];
export const defaultLocale: Locale = 'nb-NO';
```

**File:** `src/i18n/request.ts`
```typescript
import {getRequestConfig} from 'next-intl/server';

export default getRequestConfig(async ({locale}) => {
  return {
    messages: (await import(`../messages/${locale}.json`)).default
  };
});
```

---

**End of Specification**

**Next Steps:**
1. Review this spec with Alem for approval
2. Create MC task for Phase 1 implementation
3. Assign to builder agent with this spec as reference
4. Schedule external translator for Phase 2

**Questions for Alem:**
1. Approve next-intl as framework choice?
2. Defer Swedish to post-MVP or include in initial release?
3. Budget for professional legal document translation (English)?
4. Timeline preference: Fast rollout (2 weeks) vs thorough rollout (4 weeks)?