Next.jsReactTypeScripti18n

Architecture multilingue avec Next.js App Router + Static Export

Sloth255
Sloth255
·2 min read·445 words

Contexte et exigences

Nous voulons implémenter le support multilingue avec les contraintes suivantes :

  • Next.js 15 App Router avec TypeScript
  • Static Export (output: 'export') - Déploiement via S3 + CloudFront
  • Pas de rendu côté serveur - Les APIs dynamiques et les middlewares ne peuvent pas être utilisés
  • SEO requis - Doit fournir le contenu en langue appropriée aux moteurs de recherche

Aperçu de l'architecture

1. Conception du routage

/                    # Page racine (par défaut : en)
/en/                 # Page en anglais
/ja/                 # Page en japonais

Utilise le segment dynamique [locale] pour pré-rendre les pages pour chaque langue lors de la génération statique.

2. Gestion des données de traduction

Les données de traduction sont gérées au format JSON :

src/locales/en.json
{
  "title": "title"
}
src/locales/ja.json
{
  "title": "タイトル"
}

3. Gestion d'état avec l'API React Context

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 des clés imbriquées
    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. Fourniture du contexte au niveau de la hiérarchie de layout

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

Langue par défaut et redirection côté client

Côté serveur (lors de la génération statique)

La page racine (/) est toujours générée en anglais (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>
  );
}

Raisonnement :

  • Stratégie SEO : Fournir un contenu stable aux robots des moteurs de recherche
  • Contraintes d'export statique : Impossible de déterminer la langue de l'utilisateur côté serveur

Côté client (navigateur)

Lors du premier accès, redirige automatiquement après avoir détecté les paramètres de langue du navigateur :

src/components/ClientLocaleRedirect.tsx
'use client';

export default function ClientLocaleRedirect() {
  useEffect(() => {
    // Vérifier si la redirection a déjà été effectuée
    if (typeof window === 'undefined' || 
        sessionStorage.getItem('localeRedirectDone')) {
      return;
    }

    const supportedLocales = ['en', 'ja'];
    const pathname = window.location.pathname;
    
    // Rediriger uniquement pour le chemin racine
    if (pathname === '/') {
      // Obtenir les paramètres de langue du navigateur
      const browserLocales = navigator.languages || [navigator.language];
      
      // Trouver la langue supportée
      const preferredLocale = browserLocales
        .map(lang => lang.split('-')[0]) // 'ja-JP' -> 'ja'
        .find(lang => supportedLocales.includes(lang));
      
      // Rediriger si différent de la langue par défaut
      if (preferredLocale && preferredLocale !== defaultLocale) {
        sessionStorage.setItem('localeRedirectDone', 'true');
        window.location.replace(`/${preferredLocale}`);
      }
    }
  }, []);

  return null;
}

Scénarios de comportement

Situation Pendant SSG Comportement client
Robot de moteur de recherche Générer / en anglais Pas de redirection
Utilisateur avec navigateur japonais Générer / en anglais Redirection vers /ja
Utilisateur avec navigateur anglais Générer / en anglais Pas de redirection (afficher tel quel)
Utilisateur avec langue non supportée Générer / en anglais Pas de redirection (afficher en anglais)

Utilisation dans les composants

'use client';

function NetworkInfoClient() {
  const { t } = useTranslations();
  
  return (
    <div>
      <h2>{t('title')}</h2>
    </div>
  );
}

Avantages techniques

  1. Sécurité des types : TypeScript + API Context permet de vérifier l'existence des clés de traduction
  2. Dépendances claires : Les composants utilisant useTranslations doivent être placés dans le Provider, rendant la conception explicite
  3. Performance : Livraison rapide des pages via la génération statique
  4. SEO : Chaque page de langue a une URL indépendante, optimisée pour les moteurs de recherche
  5. UX : Redirection automatique basée sur les paramètres de langue du navigateur réduit l'effort de l'utilisateur

Résumé

Nous avons implémenté le support multilingue en utilisant l'API React Context dans un environnement d'export statique Next.js App Router. Nous complétons les contraintes côté serveur par une logique côté client, réalisant ainsi une bonne UX.

La modification de la langue par défaut nécessite uniquement la modification de defaultLocale dans config.ts, assurant l'extensibilité.