Multilingual Architecture with Next.js App Router + Static Export
by Sloth255
Next.jsReactTypeScripti18n
Multilingual Architecture with Next.js App Router + Static Export
Background and Requirements
We want to implement multilingual support under the following constraints:
- Next.js 15 App Router with TypeScript
- Static Export (
output: 'export') - Deployment via S3 + CloudFront - No Server-Side Rendering - Dynamic APIs and middleware cannot be used
- SEO Required - Must provide appropriate language content to search engines
Architecture Overview
1. Routing Design
/ # Root page (default: en)
/en/ # English page
/ja/ # Japanese page
Uses Dynamic Segment [locale] to pre-render pages for each language during static generation.
2. Translation Data Management
Translation data is managed in JSON format:
src/locales/en.json
{
"title": "title"
}
src/locales/ja.json
{
"title": "タイトル"
}
3. State Management with React Context API
src/i18n/context.tsx
interface I18nContextType {
messages: Record<string, any>;
locale: string;
}
const I18nContext = createContext<I18nContextType | undefined>(undefined);
export function I18nProvider({
children,
messages,
locale
}: I18nProviderProps) {
return (
<I18nContext.Provider value={{ messages, locale }}>
{children}
</I18nContext.Provider>
);
}
export function useTranslations() {
const context = useContext(I18nContext);
if (!context) {
throw new Error('useTranslations must be used within I18nProvider');
}
const t = (key: string): string => {
// Support nested keys
const keys = key.split('.');
let value: any = context.messages;
for (const k of keys) {
value = value?.[k];
}
return typeof value === 'string' ? value : key;
};
return {
t,
locale: context.locale,
messages: context.messages,
isLoaded: true
};
}
4. Context Provision at Layout Hierarchy
src/app/layout.tsx
export default async function RootLayout({ children }: Props) {
const locale = defaultLocale;
const messages = await getMessages(locale);
return (
<html lang={locale}>
<body>
<I18nProvider messages={messages} locale={locale}>
<LanguageSwitcherClient />
{children}
</I18nProvider>
</body>
</html>
);
}
src/app/[locale]/layout.tsx
export default async function LocaleLayout({ children, params }: Props) {
const { locale } = await params;
const messages = await getMessages(locale);
return (
<div lang={locale}>
<I18nProvider messages={messages} locale={locale}>
{children}
</I18nProvider>
</div>
);
}
Default Language and Client-Side Redirect
Server-Side (During Static Generation)
The root page (/) is always generated in English (en):
src/app/page.tsx
export default async function RootPage() {
const locale = defaultLocale; // 'en'
const messages = await getMessages(locale);
return (
<I18nProvider messages={messages} locale={locale}>
<ClientLocaleRedirect />
<PageContent />
</I18nProvider>
);
}
Rationale:
- SEO strategy: Provide stable content to search engine crawlers
- Static export constraints: Cannot determine user language on the server side
Client-Side (Browser)
On initial access, automatically redirects after detecting browser language settings:
src/components/ClientLocaleRedirect.tsx
'use client';
export default function ClientLocaleRedirect() {
useEffect(() => {
// Check if redirect has already been performed
if (typeof window === 'undefined' ||
sessionStorage.getItem('localeRedirectDone')) {
return;
}
const supportedLocales = ['en', 'ja'];
const pathname = window.location.pathname;
// Redirect only for root path
if (pathname === '/') {
// Get browser language settings
const browserLocales = navigator.languages || [navigator.language];
// Find supported language
const preferredLocale = browserLocales
.map(lang => lang.split('-')[0]) // 'ja-JP' -> 'ja'
.find(lang => supportedLocales.includes(lang));
// Redirect if different from default
if (preferredLocale && preferredLocale !== defaultLocale) {
sessionStorage.setItem('localeRedirectDone', 'true');
window.location.replace(`/${preferredLocale}`);
}
}
}, []);
return null;
}
Behavior Scenarios
| Situation | During SSG | Client Behavior |
|---|---|---|
| Search engine crawler | Generate / in English |
No redirect |
| Japanese browser user | Generate / in English |
Redirect to /ja |
| English browser user | Generate / in English |
No redirect (display as is) |
| Unsupported language user | Generate / in English |
No redirect (display in English) |
Usage in Components
'use client';
function NetworkInfoClient() {
const { t } = useTranslations();
return (
<div>
<h2>{t('title')}</h2>
</div>
);
}
Technical Advantages
- Type Safety: TypeScript + Context API enables translation key existence checks
- Clear Dependencies: Components using
useTranslationsmust be placed within the Provider, making the design explicit - Performance: Fast page delivery through static generation
- SEO: Each language page has an independent URL, optimized for search engines
- UX: Automatic redirect based on browser language settings reduces user effort
Summary
We have implemented multilingual support using React Context API in a Next.js App Router static export environment. We complement server-side constraints with client-side logic, achieving both SEO and UX.
Changing the default language only requires editing defaultLocale in config.ts, ensuring extensibility.