Aetherio Logo

Internationalisation (i18n) d'un SaaS : comment rendre votre app multi-langue sans tout casser

14 minutes min de lecture

Partager l'article

Introduction

L'internationalisation (i18n) d'un SaaS est l'un des chantiers les plus sous-estimés du développement logiciel. Ce n'est pas "juste de la traduction", c'est une refonte en profondeur de la manière dont votre application gère le texte, les dates, les devises, le routage et le SEO. Mal anticipée, elle peut coûter des mois de refactoring. Bien pensée dès le départ, elle ouvre votre produit à un marché mondial sans friction.

Ce guide couvre l'ensemble du processus : des fondamentaux à l'implémentation technique avec Nuxt.js et nuxt-i18n, en passant par le SEO multilingue (hreflang), la gestion des paiements multi-devises avec Stripe, et les outils de traduction à l'échelle. Vous y trouverez du code prêt à l'emploi et les pièges concrets que nous avons rencontrés sur nos projets SaaS.

Développeur présentant une carte du monde à une équipe pour l'internationalisation d'un SaaS

i18n, L10n, G11n : Comprendre les Enjeux d'un SaaS Global

Avant de plonger dans le code, clarifions trois concepts interdépendants qui structurent toute stratégie d'expansion internationale.

Internationalisation, Localisation et Globalisation

  • Internationalisation (i18n) : Le processus de conception de votre application pour qu'elle puisse être adaptée à différentes langues et régions sans modifications d'ingénierie majeures. C'est la phase de préparation technique : externaliser les chaînes de texte, gérer les formats de date dynamiquement, supporter le RTL. Pensez à l'architecture de votre SaaS, qui doit être assez flexible pour supporter de multiples locales dès le départ.
  • Localisation (L10n) : L'adaptation concrète du produit internationalisé pour une langue et une culture spécifiques. Au-delà de la traduction, cela inclut les formats de date, devises, unités de mesure, conventions d'adresse, images et même le ton éditorial.
  • Globalisation (G11n) : La stratégie qui englobe i18n + L10n : planifier quels marchés cibler, dans quel ordre, avec quels niveaux de localisation.

Pourquoi Anticiper Dès le MVP ?

Ignorer l'i18n lors de la conception de votre MVP, c'est s'exposer à des coûts 3x plus élevés au moment de l'internationalisation :

  • Refonte massive du code : chaque chaîne hardcodée doit être extraite, chaque affichage de date ou devise réécrit
  • Risque d'erreurs : les oublis mènent à des bugs d'affichage coûteux en réputation
  • Impact UX : une app mal internationalisée frustre les utilisateurs locaux et les pousse vers la concurrence
  • Temps perdu : vos équipes passent des mois à corriger des problèmes fondamentaux au lieu de développer des features

En intégrant l'i18n dès le début, vous posez les fondations d'une architecture multi-tenant robuste, prête à accueillir de nouvelles langues avec agilité.

Les Pièges Classiques de l'Internationalisation

Au-delà de la Simple Traduction

La traduction des chaînes de caractères n'est que la surface. Votre application doit aussi gérer :

  • Pluriels et grammaire : "Il y a n utilisateur(s)" ne se traduit pas de la même manière en arabe (6 formes de pluriel), en polonais (3 formes) ou en japonais (pas de pluriel). Les systèmes modernes basés sur ICU Message Format gèrent ces cas.
  • Formats de date : MM/DD/YYYY (US) vs DD/MM/YYYY (EU) vs YYYY/MM/DD (Asie). Ne jamais hardcoder.
  • Formats numériques et devises : 1,234.56 (US) vs 1.234,56 (EU). La position du symbole monétaire varie ($100 vs 100 €).
  • RTL (Right-To-Left) : L'arabe et l'hébreu inversent toute l'interface : navigation, formulaires, icônes directionnelles. Votre CSS doit être conçu pour le supporter.
  • Images contenant du texte : Impossible à traduire automatiquement. Privilégiez les icônes universelles ou le texte superposable en HTML/CSS.
  • Tri et collation : L'ordre alphabétique varie (Å en suédois n'est pas à la même position qu'en français). Le tri des listes doit utiliser Intl.Collator.

Concaténation : L'Erreur la Plus Fréquente

// ❌ Ne faites JAMAIS ça
const message = "Bienvenue " + user.name + ", vous avez " + count + " messages";

// ✅ Utilisez des clés de traduction avec interpolation
// fr.json: { "welcome": "Bienvenue {name}, vous avez {count} message | messages" }
t('welcome', { name: user.name, count })

L'ordre des mots change entre les langues. En japonais, le nom vient souvent en premier. En arabe, la structure de phrase est complètement différente. Toute concaténation manuelle cassera inévitablement dans au moins une langue.

Implémentation Technique avec Nuxt.js et nuxt-i18n

@nuxtjs/i18n est le module de référence pour l'internationalisation d'applications Nuxt.js. Construit sur vue-i18n, il gère le routage multilingue, le lazy loading des traductions et la détection automatique de la langue.

Installation et Configuration de Base

npx nuxi module add @nuxtjs/i18n
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxtjs/i18n'],

  i18n: {
    locales: [
      {
        code: 'fr',
        language: 'fr-FR',
        file: 'fr.json',
        name: 'Français'
      },
      {
        code: 'en',
        language: 'en-US',
        file: 'en.json',
        name: 'English'
      },
      {
        code: 'de',
        language: 'de-DE',
        file: 'de.json',
        name: 'Deutsch'
      }
    ],
    defaultLocale: 'fr',
    lazy: true,
    langDir: 'locales/',
    strategy: 'prefix_except_default'
  }
})

Stratégies de Routage : Quel Préfixe Choisir ?

Le choix de la stratégie de routage impacte directement votre SEO et votre UX :

StratégieURL FRURL ENSEOCas d'usage
prefix_except_default/pricing/en/pricingExcellentSaaS avec une langue principale
prefix/fr/pricing/en/pricingExcellentMarchés équivalents, pas de langue par défaut
no_prefix/pricing/pricingMauvaisApps internes, pas de SEO nécessaire

Pour un SaaS ciblant l'international, prefix_except_default est généralement le meilleur choix : votre marché principal conserve des URLs propres, et les marchés secondaires ont des URLs clairement localisées.

Fichiers de Traduction et Interpolation

// locales/fr.json
{
  "nav": {
    "home": "Accueil",
    "pricing": "Tarifs",
    "login": "Connexion"
  },
  "dashboard": {
    "welcome": "Bienvenue {name}",
    "projects_count": "Aucun projet | {count} projet | {count} projets",
    "last_login": "Dernière connexion : {date}"
  },
  "billing": {
    "plan_price": "{price} / mois",
    "trial_remaining": "Il vous reste {days} jour d'essai | Il vous reste {days} jours d'essai"
  }
}
// locales/en.json
{
  "nav": {
    "home": "Home",
    "pricing": "Pricing",
    "login": "Sign in"
  },
  "dashboard": {
    "welcome": "Welcome {name}",
    "projects_count": "No projects | {count} project | {count} projects",
    "last_login": "Last login: {date}"
  },
  "billing": {
    "plan_price": "{price} / month",
    "trial_remaining": "{days} trial day remaining | {days} trial days remaining"
  }
}

Utilisation dans les Composants Vue

<template>
  <div>
    <h1>{{ $t('dashboard.welcome', { name: user.name }) }}</h1>
    <p>{{ $t('dashboard.projects_count', { count: projects.length }, projects.length) }}</p>
    <p>{{ $t('dashboard.last_login', { date: d(user.lastLogin, 'short') }) }}</p>

    <!-- Sélecteur de langue -->
    <select @change="switchLocale($event.target.value)">
      <option
        v-for="locale in availableLocales"
        :key="locale.code"
        :value="locale.code"
        :selected="locale.code === $i18n.locale"
      >
        {{ locale.name }}
      </option>
    </select>
  </div>
</template>

<script setup>
const { locale, locales, setLocale } = useI18n()
const switchLocalePath = useSwitchLocalePath()
const availableLocales = computed(() => locales.value)

async function switchLocale(code) {
  await setLocale(code)
  await navigateTo(switchLocalePath(code))
}
</script>

Formatage des Dates et Nombres avec Intl

Ne réinventez pas la roue : utilisez l'API Intl du navigateur, que vue-i18n intègre nativement :

// nuxt.config.ts : configuration des formats
i18n: {
  datetimeFormats: {
    fr: {
      short: { year: 'numeric', month: '2-digit', day: '2-digit' },
      long: { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long' }
    },
    en: {
      short: { year: 'numeric', month: '2-digit', day: '2-digit' },
      long: { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long' }
    }
  },
  numberFormats: {
    fr: {
      currency: { style: 'currency', currency: 'EUR' }
    },
    en: {
      currency: { style: 'currency', currency: 'USD' }
    }
  }
}
<!-- Résultat : "15/03/2026" en FR, "03/15/2026" en EN -->
<p>{{ d(date, 'short') }}</p>

<!-- Résultat : "49,00 €" en FR, "$49.00" en EN -->
<p>{{ n(4900, 'currency') }}</p>

SEO Multilingue : hreflang, Sitemap et Structure d'URL

L'internationalisation sans SEO multilingue, c'est construire un magasin sans y afficher d'enseigne dans la langue du quartier. Google doit comprendre quelle version de votre page servir à quel utilisateur.

Balises hreflang

Les balises hreflang indiquent aux moteurs de recherche les relations entre les versions linguistiques d'une même page. Sans elles, Google peut considérer vos pages traduites comme du contenu dupliqué.

<!-- Dans le <head> de chaque page -->
<link rel="alternate" hreflang="fr" href="https://votresaas.com/pricing" />
<link rel="alternate" hreflang="en" href="https://votresaas.com/en/pricing" />
<link rel="alternate" hreflang="de" href="https://votresaas.com/de/pricing" />
<link rel="alternate" hreflang="x-default" href="https://votresaas.com/pricing" />

Avec nuxt-i18n, ces balises sont générées automatiquement via le composable useHead et la configuration du module. Assurez-vous que baseUrl est configuré dans votre nuxt.config.ts :

i18n: {
  baseUrl: 'https://votresaas.com'
}

Structure d'URL : Sous-répertoire vs Sous-domaine vs Domaine

StructureExempleSEOMaintenanceRecommandation
Sous-répertoirevotresaas.com/en/Hérite de l'autorité du domaineFacile — un seul déploiementRecommandé pour la majorité des SaaS
Sous-domaineen.votresaas.comAutorité séparée, SEO plus lentMoyenne — configurations DNSGrand volume, équipes locales dédiées
Domaine séparévotresaas.co.ukCiblage géo fort, autorité à construireComplexe — plusieurs déploiementsMarchés très spécifiques (Chine, Russie)

Pour un SaaS en croissance, les sous-répertoires (/en/, /de/) sont le choix optimal : vous conservez toute l'autorité SEO de votre domaine principal, et le déploiement reste simple. C'est d'ailleurs la stratégie que nous utilisons pour nos propres projets et recommandons dans notre guide sur le SEO technique.

Sitemap Multilingue

Générez un sitemap XML incluant les balises xhtml:link pour chaque version linguistique :

<url>
  <loc>https://votresaas.com/pricing</loc>
  <xhtml:link rel="alternate" hreflang="fr" href="https://votresaas.com/pricing"/>
  <xhtml:link rel="alternate" hreflang="en" href="https://votresaas.com/en/pricing"/>
  <xhtml:link rel="alternate" hreflang="de" href="https://votresaas.com/de/pricing"/>
</url>

Meta Tags Localisés

Chaque version linguistique doit avoir ses propres title et meta description, pas une simple traduction automatique, mais un contenu optimisé pour les requêtes de recherche locales :

// pages/pricing.vue
useHead({
  title: computed(() => t('pricing.meta_title')),
  meta: [
    { name: 'description', content: computed(() => t('pricing.meta_description')) }
  ]
})

Paiements Multi-Devises avec Stripe

L'internationalisation du billing est un sujet à part entière. Afficher des prix en euros à un client américain, c'est ajouter de la friction au moment le plus critique de votre funnel. Pour une intégration complète de Stripe dans votre SaaS, consultez notre guide dédié à l'intégration Stripe.

Stripe Multi-Currency : Les Fondamentaux

Stripe supporte nativement plus de 135 devises. La clé est de créer vos produits et prix avec des variantes par devise :

// Créer un prix en EUR et USD pour le même produit
const priceEUR = await stripe.prices.create({
  product: 'prod_xxx',
  unit_amount: 4900, // 49,00 €
  currency: 'eur',
  recurring: { interval: 'month' }
});

const priceUSD = await stripe.prices.create({
  product: 'prod_xxx',
  unit_amount: 5200, // $52.00
  currency: 'usd',
  recurring: { interval: 'month' }
});

Mapping Locale → Devise

const LOCALE_CURRENCY_MAP: Record<string, { currency: string; priceId: string }> = {
  'fr': { currency: 'eur', priceId: 'price_eur_monthly' },
  'en': { currency: 'usd', priceId: 'price_usd_monthly' },
  'de': { currency: 'eur', priceId: 'price_eur_monthly' },
  'ja': { currency: 'jpy', priceId: 'price_jpy_monthly' }
};

function getPriceForLocale(locale: string) {
  return LOCALE_CURRENCY_MAP[locale] ?? LOCALE_CURRENCY_MAP['en'];
}

TVA et Taxes par Pays

La gestion de la TVA en multi-pays est un piège majeur. Stripe Tax automatise le calcul, mais vous devez tout de même :

  • Identifier le pays du client (via l'adresse de facturation, pas la locale du navigateur)
  • Appliquer le bon taux de TVA (20 % en France, 19 % en Allemagne, 0 % hors UE pour le B2B)
  • Générer des factures conformes à chaque juridiction

Pour la conformité RGPD et les obligations légales par pays, un accompagnement juridique est fortement recommandé dès que vous dépassez 2-3 marchés.

Gestion des Traductions à l'Échelle

Workflow de Traduction : Du Développeur au Traducteur

Un workflow i18n mature sépare clairement les responsabilités :

Développeur          →    Plateforme i18n    →    Traducteur
                          (Crowdin, Lokalise)
Ajoute clé "new_feature"  Synchronise les     Traduit dans
dans fr.json               fichiers JSON        chaque langue
                           ↕                    ↕
                     Pull Request auto     Validation contextuelle
                     avec traductions      (screenshots, contexte)

Outils de Gestion des Traductions (TMS)

OutilForcesPrixIdéal pour
CrowdinIntégration GitHub/GitLab, over-the-air updatesGratuit (open source), 40 $/mois (pro)SaaS en croissance, communauté
LokaliseUI intuitive, QA automatisé, branching120 $/moisÉquipes produit avec traducteurs dédiés
PhraseAPI puissante, gestion de contexte avancéeSur devisEntreprises multi-produits
i18next + fichiers JSONGratuit, pas de dépendance externe0 €MVP, < 3 langues

Intégration CI/CD

Automatisez la synchronisation des traductions dans votre pipeline CI/CD :

# .github/workflows/i18n-sync.yml
name: Sync translations
on:
  push:
    paths:
      - 'locales/fr.json'

jobs:
  sync:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Upload source to Crowdin
        uses: crowdin/github-action@v2
        with:
          upload_sources: true
          download_translations: false
        env:
          CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
          CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_TOKEN }}
# Workflow inverse : télécharger les traductions
name: Download translations
on:
  schedule:
    - cron: '0 6 * * 1' # Tous les lundis à 6h

jobs:
  download:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Download from Crowdin
        uses: crowdin/github-action@v2
        with:
          upload_sources: false
          download_translations: true
          create_pull_request: true
          pull_request_title: 'chore(i18n): update translations'

Bonnes Pratiques pour les Fichiers de Traduction

  • Organisez par feature, pas par page : dashboard.welcome plutôt que page_dashboard.text_1
  • Clés en anglais snake_case : lisibles par les développeurs, indépendantes de la langue source
  • Pas de HTML dans les traductions : utilisez les composants <i18n-t> de vue-i18n pour le markup
  • Contexte pour les traducteurs : ajoutez des commentaires dans votre TMS pour les chaînes ambiguës
  • Tests de longueur : l'allemand est ~30 % plus long que l'anglais, le japonais ~50 % plus court. Votre UI doit s'adapter.

Tester l'Internationalisation

L'i18n est une source de régressions silencieuses. Intégrez ces vérifications dans vos tests automatisés :

// Vérifier qu'aucune clé de traduction ne manque
import fr from '../locales/fr.json';
import en from '../locales/en.json';

function getKeys(obj: Record<string, any>, prefix = ''): string[] {
  return Object.entries(obj).flatMap(([key, value]) =>
    typeof value === 'object'
      ? getKeys(value, `${prefix}${key}.`)
      : [`${prefix}${key}`]
  );
}

describe('i18n completeness', () => {
  it('en.json has all keys from fr.json', () => {
    const frKeys = getKeys(fr);
    const enKeys = getKeys(en);
    const missing = frKeys.filter(k => !enKeys.includes(k));
    expect(missing).toEqual([]);
  });
});

Testez aussi visuellement avec des pseudo-traductions (remplacer chaque caractère par un accent : Ãççöûñt Sëttîñgs) pour repérer les textes hardcodés oubliés et les problèmes de débordement.

FAQ : Internationalisation d'un SaaS

FAQ - Questions fréquentes

Conclusion : L'i18n, un Investissement Stratégique

L'internationalisation n'est pas un chantier technique isolé, c'est un levier de croissance stratégique. Un SaaS bien internationalisé peut multiplier son marché adressable par 5 ou 10 en ciblant l'anglais, l'allemand et l'espagnol en plus du français. Les outils modernes (nuxt-i18n, Crowdin, Stripe Tax) réduisent considérablement la friction technique, mais la réussite repose sur une décision architecturale prise tôt : externaliser les textes, structurer le routage multilingue, et prévoir la gestion des devises dès la conception.

Si vous envisagez d'internationaliser votre SaaS ou de créer un SaaS de zéro avec l'international en tête, contactez notre équipe. Nous concevons des applications web sur mesure et des SaaS prêts pour l'international dès le premier jour.