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:


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, i18n library comparison

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)
  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)
  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, Next.js 16 i18n guide


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

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:

Date Formatting:

Number Formatting:


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

{
  "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

{
  "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):

<span className="text-xs text-[#1E293B]">Hjem</span>

Example (After):

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

Example (API Route — Before):

return jsonError("unauthorized", "Invalid credentials", 401);

Example (API Route — After):

return jsonError("unauthorized", "errors.invalid_credentials", 401);
// Note: Second param is translation KEY, not message

Example (Frontend):

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

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:

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

Scope: Terms of Service, Privacy Policy, Fee Schedule

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/
        └── ...
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):

English Locale (en):

Swedish Locale (sv):

Implementation (next-intl):

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, Norwegian Bokmål locale formatting

6.2 Date & Time Formatting

Norwegian (nb-NO):

English (en):

Implementation:

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

English (en):

Implementation:

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.jsonen.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:

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:

Example:

Database:

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

Display (React component):

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:

{
  "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:

Example:

{
  "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:

Pros:

Cons:

Implementation:

File: src/middleware.ts

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

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

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:

ALTER TABLE users ADD COLUMN preferred_language TEXT DEFAULT 'nb-NO';

API Route to Update Preference:

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

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

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

Compliance:

Requirement: User must consent in a language they understand.

Strategy:

Database:

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:


11. Testing Strategy

11.1 Unit Tests

Framework: Vitest (already in use)

Test Files:

Example:

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:

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:

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:

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:

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


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 or Crowdin 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

Localization Best Practices

Library Comparisons


18. Appendix: Translation File Examples

A. Complete nb-NO.json (Sample)

See Section 4.2 for full structure.

B. ESLint Rule for Hardcoded Strings

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

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

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

Revision #4
Created 2026-02-18 08:44:45 UTC by John
Updated 2026-05-31 20:02:17 UTC by John