i18n

Système d'Internationalisation (i18n)

Cette documentation présente le système d'internationalisation complet de l'application Nuxt 3, incluant la configuration, l'organisation des traductions, et les bonnes pratiques d'utilisation.

Vue d'Ensemble du Système i18n

L'application utilise le module @nuxtjs/i18n pour gérer la localisation et les traductions. Ce système permet de supporter plusieurs langues avec une navigation automatique, des URLs localisées, et une gestion avancée des traductions.

graph TB
    subgraph "Configuration i18n"
        Config[nuxt.config.ts]
        Locales[Fichiers de Langues]
        Strategy[Stratégie d'URL]
    end

    subgraph "Utilisation"
        Components[Composants Vue]
        Pages[Pages/Routes]
        Server[Routes Serveur]
    end

    subgraph "APIs i18n"
        UseI18n[useI18n()]
        T["$t()"]
        LocalePath[localePath()]
        SwitchPath[switchLocalePath()]
    end

    Config --> UseI18n
    Locales --> T
    Strategy --> LocalePath

    Components --> UseI18n
    Components --> T
    Pages --> LocalePath
    Server --> Config

    UseI18n --> SwitchPath
    T --> Components
    LocalePath --> Pages

    style Config fill:#e1f5fe
    style Locales fill:#f3e5f5
    style Components fill:#e8f5e8
    style UseI18n fill:#fff3e0

Configuration du Système

Configuration Nuxt.config.ts

// nuxt.config.ts
export default defineNuxtConfig({
  modules: [
    "@nuxtjs/i18n",
    // ... autres modules
  ],

  i18n: {
    // Chargement paresseux des traductions
    lazy: true,

    // Dossier contenant les fichiers de langue
    langDir: "locales",

    // Stratégie d'URL : préfixe sauf pour la langue par défaut
    strategy: "prefix_except_default",

    // Langue par défaut basée sur la variable d'environnement
    defaultLocale: process.env.LOCALE === "en-GB" ? "en-GB" : "fr-FR",

    // Langues supportées
    locales: [
      {
        code: "fr-FR",
        file: "fr-FR.json",
        label: "Français",
        icon: "cif:fr",
        abbr: "fr",
      },
      {
        code: "en-GB",
        file: "en-GB.json",
        label: "English",
        icon: "cif:gb",
        abbr: "en",
      },
    ],
  },
});

Variables d'Environnement

# .env
LOCALE=fr-FR                    # Langue par défaut
DEFAULTCURRENCY=EUR             # Devise par défaut
CURRENCIES=EUR,USD,GBP          # Devises supportées

Organisation des Fichiers de Traduction

Structure des Dossiers

i18n/
└── locales/
    ├── fr-FR.json              # Traductions françaises (22KB, 639 lignes)
    └── en-GB.json              # Traductions anglaises (19KB, 637 lignes)

Structure des Clés de Traduction

Les traductions sont organisées par composant pour une meilleure maintenabilité :

{
  "DjAddToBuyingListButton": {
    "addFavoris": "Ajouter aux favoris",
    "selectList": "Sélectionner une liste de favoris existante :",
    "placeholder": "Choisir une liste de favoris",
    "createList": "Je créé une nouvelle liste de favoris :",
    "namePlaceholder": "Nom de votre liste de favoris",
    "create": "Crée"
  },

  "DjCartPage": {
    "cart": "Panier",
    "details": "Détails de Livraison & Facturation",
    "order": "Commande passée"
  },

  "DjContactUsPage": {
    "title": "Nous contacter",
    "name": "Nom",
    "email": "Email",
    "message": "Message",
    "mandatory": "*",
    "topic": {
      "label": "Sujet",
      "product": "Question produit",
      "order": "Question commande",
      "delivery": "Question livraison",
      "other": "Autre"
    },
    "info": {
      "one": "Pour toute question,",
      "two": "notre équipe vous répond"
    },
    "submitForm": "Envoyer"
  }
}

Stratégies d'URL et Routage

Stratégie prefix_except_default

Cette stratégie génère des URLs différentes selon la langue :

// Langue par défaut (fr-FR) : pas de préfixe
https://example.com/products
https://example.com/cart
https://example.com/account

// Autres langues (en-GB) : avec préfixe
https://example.com/en-GB/products
https://example.com/en-GB/cart
https://example.com/en-GB/account

Navigation Localisée

<template>
  <!-- Lien vers une page dans la langue courante -->
  <NuxtLink :to="localePath('/products')">
    {{ $t("navigation.products") }}
  </NuxtLink>

  <!-- Lien vers la même page dans une autre langue -->
  <NuxtLink :to="switchLocalePath('en-GB')">
    {{ $t("navigation.english") }}
  </NuxtLink>
</template>

<script setup>
const localePath = useLocalePath();
const switchLocalePath = useSwitchLocalePath();
</script>

Utilisation dans les Composants

Méthodes d'Accès aux Traductions

1. Template avec $t() (Plus Simple)

<template>
  <div>
    <!-- Traduction simple -->
    <h1>{{ $t("DjCartPage.cart") }}</h1>

    <!-- Traduction avec interpolation -->
    <p>{{ $t("DjFaq.phone", { phone: "01 23 45 67 89" }) }}</p>

    <!-- Traduction plurielle -->
    <span>{{
      $t(
        "DjFastCartPage.nbProducts",
        { nb: cartLines.length },
        cartLines.length
      )
    }}</span>
  </div>
</template>

2. Composition API avec useI18n() (Plus Flexible)

<script setup lang="ts">
const { t, locale, locales } = useI18n();

// Utilisation dans les variables réactives
const pageTitle = computed(() => t("DjContactUsPage.title"));

// Utilisation dans les tableaux
const topicOptions = [
  t("DjContactUsPage.topic.product"),
  t("DjContactUsPage.topic.order"),
  t("DjContactUsPage.topic.delivery"),
  t("DjContactUsPage.topic.other"),
];

// Utilisation dans les schémas de validation
const schema = v.object({
  email: v.pipe(
    v.string(),
    v.email(t("validation.email")),
    v.nonEmpty(t("validation.required"))
  ),
});
</script>

Exemples Pratiques d'Utilisation

Formulaires avec Validation

<template>
  <UForm :schema="schema" :state="state" @submit="onSubmit">
    <UFormGroup :label="$t('DjContactUsPage.name')" name="name">
      <UInput v-model="state.name" :placeholder="$t('DjContactUsPage.name')" />
    </UFormGroup>

    <UButton type="submit">
      {{ $t("DjContactUsPage.submitForm") }}
    </UButton>
  </UForm>
</template>

<script setup lang="ts">
import * as v from "valibot";

const { t } = useI18n();

const schema = v.object({
  name: v.pipe(v.string(), v.nonEmpty(t("validation.required"))),
  email: v.pipe(v.string(), v.email(t("validation.email"))),
});

const state = reactive({
  name: "",
  email: "",
});
</script>

Tableaux avec Colonnes Traduites

<script setup lang="ts">
const { t } = useI18n();

const columns = [
  {
    accessorKey: "reference",
    header: t("DjOrdersPage.column.reference"),
  },
  {
    accessorKey: "quantity",
    header: t("DjOrdersPage.column.quantity"),
  },
  {
    accessorKey: "totalHT",
    header: t("DjOrdersPage.column.totalHT"),
  },
  {
    accessorKey: "status",
    header: t("DjOrdersPage.column.status"),
  },
];

// Status traduit dynamiquement
const statusLabel = computed(() => t(`orderStatus.${order.status}`));
</script>

Commutateur de Langue

Composant DjLangSwitcher

<template>
  <USelectMenu
    v-if="availableLocales"
    v-model="selectedLocale"
    :items="availableLocales"
  >
    <UButton variant="ghost" class="flex items-center">
      <UIcon :name="selectedLocale.icon" class="w-5 h-5 mr-2" />
      <span class="uppercase">{{ selectedLocale?.abbr }}</span>
    </UButton>
  </USelectMenu>
</template>

<script setup>
const { locale, locales } = useI18n();
const switchLocalePath = useSwitchLocalePath();
const router = useRouter();

const availableLocales = computed(() => locales.value);

const selectedLocale = ref(locales.value.find((i) => i.code === locale.value));

// Changement de langue automatique
watch([selectedLocale], () => {
  router.push(switchLocalePath(selectedLocale.value.code));
});
</script>

Intégration Côté Serveur

APIs avec Support i18n

// server/api/categories/index.get.ts
export default defineEventHandler(async (event) => {
  const query = getQuery(event);
  const runtimeConfig = useRuntimeConfig();

  // Récupération de la locale depuis la query ou la config
  const localeEnv = runtimeConfig.public.locale as string;
  const locale =
    typeof query.locale === "string" && query.locale.trim().length > 0
      ? query.locale
      : localeEnv;

  if (!locale) {
    throw createError({
      statusCode: 400,
      statusMessage: "Locale is required and could not be resolved.",
    });
  }

  // Utilisation de la locale pour les appels SDK
  const categories = await sdk.getNavigationCategories({ locale });

  return { success: true, categories };
});

Middleware avec Détection de Langue

// middleware/i18n.global.ts
export default defineNuxtRouteMiddleware((to) => {
  const { locale, defaultLocale } = useI18n();

  // Redirection si locale non supportée
  if (!["fr-FR", "en-GB"].includes(locale.value)) {
    return navigateTo(`/${defaultLocale}/`);
  }
});
// Schéma de validation avec messages i18n
const { t } = useI18n();

const schema = v.object({
  email: v.pipe(
    v.string(),
    v.email(t("validation.email")),
    v.nonEmpty(t("validation.required"))
  ),
  password: v.pipe(
    v.string(),
    v.minLength(8, t("validation.password.minLength", { min: 8 })),
    v.nonEmpty(t("validation.required"))
  ),
});

Bonnes Pratiques

1. Organisation des Clés

✅ Bonne Pratique : Par Composant

{
  "DjProductCard": {
    "addToCart": "Ajouter au panier",
    "viewDetails": "Voir les détails",
    "outOfStock": "Rupture de stock"
  }
}

❌ À Éviter : Clés Plates

{
  "addToCart": "Ajouter au panier",
  "productViewDetails": "Voir les détails",
  "productOutOfStock": "Rupture de stock"
}

2. Interpolation et Pluralisation

Variables dans les Traductions

{
  "welcome": "Bienvenue {name}",
  "itemsCount": "{count} article(s) | {count} article | {count} articles"
}
<template>
  <!-- Variable simple -->
  <p>{{ $t("welcome", { name: user.firstName }) }}</p>

  <!-- Pluralisation -->
  <p>{{ $t("itemsCount", { count: items.length }, items.length) }}</p>
</template>

Configuration Optimisée

// nuxt.config.ts
i18n: {
  // Chargement paresseux des traductions
  lazy: true,

  // Préchargement de certaines langues
  precompile: {
    strictMessage: false,
  },

  // Compilation des messages pour la performance
  compilation: {
    strictMessage: false,
  }
}

Génération Automatique des Types

// types/i18n.ts
export interface I18nMessages {
  DjCartPage: {
    cart: string;
    details: string;
    order: string;
  };
  DjContactUsPage: {
    title: string;
    name: string;
    email: string;
    topic: {
      label: string;
      product: string;
      order: string;
    };
  };
}

// Utilisation avec autocomplétion
const { t } = useI18n<{ message: I18nMessages }>();

Flux Complet d'Internationalisation

Exemple : Ajout d'une Nouvelle Fonctionnalité

sequenceDiagram
    participant Dev as Développeur
    participant FR as fr-FR.json
    participant EN as en-GB.json
    participant Comp as Composant Vue
    participant User as Utilisateur

    Dev->>FR: Ajouter clés françaises
    Dev->>EN: Ajouter clés anglaises
    Dev->>Comp: Utiliser $t() ou useI18n()
    Comp->>FR: Charger traductions (locale=fr)
    Comp->>EN: Charger traductions (locale=en)
    Comp->>User: Afficher interface traduite
    User->>Comp: Changer de langue
    Comp->>Comp: Recharger avec nouvelle locale
    Comp->>User: Interface mise à jour

1. Ajout des Traductions

// i18n/locales/fr-FR.json
{
  "DjNewFeature": {
    "title": "Nouvelle Fonctionnalité",
    "description": "Description de la fonctionnalité",
    "action": "Action principale",
    "success": "Opération réussie",
    "error": "Une erreur s'est produite"
  }
}
// i18n/locales/en-GB.json
{
  "DjNewFeature": {
    "title": "New Feature",
    "description": "Feature description",
    "action": "Main action",
    "success": "Operation successful",
    "error": "An error occurred"
  }
}

2. Utilisation dans le Composant

<template>
  <div class="dj-new-feature">
    <h2>{{ $t("DjNewFeature.title") }}</h2>
    <p>{{ $t("DjNewFeature.description") }}</p>
    <UButton @click="handleAction">
      {{ $t("DjNewFeature.action") }}
    </UButton>
  </div>
</template>

<script setup lang="ts">
const { t } = useI18n();
const toast = useToast();

const handleAction = async () => {
  try {
    await performAction();
    toast.add({
      title: t("DjNewFeature.success"),
      color: "green",
    });
  } catch (error) {
    toast.add({
      title: t("DjNewFeature.error"),
      color: "red",
    });
  }
};
</script>