Appearance
🌐 Système i18n (Internationalisation)
Vue d'ensemble
J'ai créé un système de traduction custom léger basé sur des fichiers JSON statiques. Contrairement à des librairies comme astro-i18next, mon système est minimaliste et adapté à mes besoins.
Avantages :
- 🪶 Léger (~100 lignes de code)
- 📦 Pas de dépendance externe
- 🎯 Parfaitement adapté à mes besoins
- 🚀 Performance optimale
Architecture
Fichiers de Traduction
public/locales/
├── en/
│ ├── common.json # Textes généraux (theme, locale, keyboard)
│ ├── components.json # Composants UI
│ └── sections.json # Sections CV
└── fr/
├── common.json
├── components.json
└── sections.jsonFonction Principale : t()
Fichier : src/utils/i18n.ts
Signature :
typescript
function t(
key: string, // Format: "namespace:path.to.key"
options: {
lng: Locale; // "fr" | "en"
[key: string]: any // Variables pour interpolation
}
): stringUtilisation
Import
astro
---
import { t } from "@/utils/i18n";
---Traduction Simple
astro
---
const title = t("sections:about.title", { lng: "fr" });
// → "À propos"
---
<h2>{title}</h2>Traduction avec Interpolation
astro
---
const name = "Simon";
const phone = "+33 X XX XX XX XX";
const text = t("sections:hero.phoneTitle", {
lng: "fr",
name,
phone
});
// → "Contacter Simon au +33 X XX XX XX XX"
---
<a href={`tel:${phone}`} title={text}>
{phone}
</a>Format des Clés
Syntaxe : namespace:path.to.key
Namespace = Nom du fichier JSON (sans .json) Path = Chemin dans l'objet JSON (séparé par des points)
Exemples
Fichier : public/locales/fr/sections.json
json
{
"hero": {
"showProfile": "Voir le profil de {{name}} sur"
},
"experience": {
"title": "Expérience professionnelle",
"now": "Actuel"
}
}Utilisation :
typescript
t("sections:hero.showProfile", { lng: "fr", name: "Simon" })
// → "Voir le profil de Simon sur"
t("sections:experience.title", { lng: "fr" })
// → "Expérience professionnelle"
t("sections:experience.now", { lng: "fr" })
// → "Actuel"Interpolation de Variables
Syntaxe :
Dans les fichiers JSON, j'utilise pour indiquer où insérer des données dynamiques.
Exemple :
json
{
"hero": {
"showProfile": "Voir le profil de {{name}} sur",
"phoneTitle": "Contacter {{name}} au {{phone}}"
}
}Code :
typescript
t("sections:hero.showProfile", {
lng: "fr",
name: "Simon"
});
// → "Voir le profil de Simon sur"
t("sections:hero.phoneTitle", {
lng: "fr",
name: "Simon",
phone: "+33 6 12 34 56 78"
});
// → "Contacter Simon au +33 6 12 34 56 78"Fonctionnement Interne
typescript
// Regex pour trouver toutes les occurrences de {{variable}}
const regex = /\{\{(\w+)\}\}/g;
// Remplacer chaque {{variable}} par sa valeur
translation = translation.replace(regex, (match, varName) => {
return options[varName] ?? match;
});Exemple :
Input: "Voir le profil de {{name}} sur {{network}}"
Options: { name: "Simon", network: "GitHub" }
Output: "Voir le profil de Simon sur GitHub"Fonctionnement Interne
Étapes de Traduction
Parse la clé :
"sections:hero.showProfile"- Namespace =
sections - Path =
hero.showProfile
- Namespace =
Charge le fichier JSON :
typescriptconst translation = await import( `/public/locales/${lng}/${namespace}.json` );Navigate dans l'objet :
typescriptlet result = translation; for (const key of path.split(".")) { result = result[key]; } // result = "Voir le profil de {{name}} sur"Interpole les variables :
typescriptresult = result.replace(/\{\{(\w+)\}\}/g, (match, varName) => { return options[varName] ?? match; }); // result = "Voir le profil de Simon sur"Retourne la string traduite
Code Source Complet
Fichier : src/utils/i18n.ts
typescript
export type Locale = "en" | "fr";
interface TranslationOptions {
lng: Locale;
[key: string]: any;
}
// Cache pour éviter de recharger les mêmes fichiers
const translationsCache: Record<string, any> = {};
export async function t(
key: string,
options: TranslationOptions
): Promise<string> {
const { lng, ...variables } = options;
// Parse la clé (format: "namespace:path.to.key")
const [namespace, path] = key.split(":");
if (!namespace || !path) {
if (import.meta.env.DEV) {
console.warn(`Clé de traduction invalide: ${key}`);
}
return key;
}
// Vérifie que la locale est valide
if (!["en", "fr"].includes(lng)) {
if (import.meta.env.DEV) {
console.warn(`Locale invalide: ${lng}`);
}
return key;
}
// Charge le fichier de traduction (avec cache)
const cacheKey = `${lng}-${namespace}`;
if (!translationsCache[cacheKey]) {
try {
const module = await import(
`/public/locales/${lng}/${namespace}.json`
);
translationsCache[cacheKey] = module.default || module;
} catch (error) {
if (import.meta.env.DEV) {
console.warn(
`Fichier de traduction introuvable: ${lng}/${namespace}.json`
);
}
return key;
}
}
// Navigate dans l'objet JSON avec le path
let translation = translationsCache[cacheKey];
for (const segment of path.split(".")) {
translation = translation?.[segment];
if (translation === undefined) {
if (import.meta.env.DEV) {
console.warn(`Traduction introuvable: ${key}`);
}
return key;
}
}
// Interpole les variables
if (typeof translation === "string") {
translation = translation.replace(/\{\{(\w+)\}\}/g, (match, varName) => {
return variables[varName] ?? match;
});
}
return translation;
}
// Version synchrone pour les cas où on a déjà chargé les traductions
export function tSync(key: string, options: TranslationOptions): string {
// Même logique mais sans async/await
// Utilisé uniquement si les traductions sont déjà en cache
}Mode DEV vs Production
Console Warnings
Les warnings ne s'affichent qu'en mode développement :
typescript
if (import.meta.env.DEV) {
console.warn("Message de debug");
}En DEV (pnpm dev) :
- ✅ Warnings visibles dans la console
- 🐛 Facilite le debugging
En PROD (pnpm build) :
- ✅ Aucun warning dans la console
- 🚀 Console propre pour l'utilisateur
Types de Warnings
Clé invalide :
Clé de traduction invalide: invalidKeyLocale invalide :
Locale invalide: deFichier introuvable :
Fichier de traduction introuvable: fr/missing.jsonTraduction introuvable :
Traduction introuvable: sections:nonexistent.key
Cache des Traductions
Pourquoi un Cache ?
Pour éviter de recharger les mêmes fichiers JSON à chaque appel de t().
typescript
const translationsCache: Record<string, any> = {};Clé de Cache
Format : ${locale}-${namespace}
Exemples :
fr-commonen-sectionsfr-components
Fonctionnement
typescript
const cacheKey = `${lng}-${namespace}`;
if (!translationsCache[cacheKey]) {
// Charge le fichier (1ère fois seulement)
const module = await import(`/public/locales/${lng}/${namespace}.json`);
translationsCache[cacheKey] = module.default || module;
}
// Utilise le cache pour les appels suivants
let translation = translationsCache[cacheKey];Bénéfice : Performance optimale, chaque fichier n'est chargé qu'une seule fois.
Ajouter une Nouvelle Traduction
1. Identifier le Namespace
- common : Textes généraux (theme, locale, keyboard)
- components : Composants UI (dialog, buttons)
- sections : Sections CV (hero, about, experience, etc.)
2. Ajouter dans les 2 Langues
Français (public/locales/fr/sections.json) :
json
{
"myNewSection": {
"title": "Mon Nouveau Titre",
"description": "Description en français"
}
}Anglais (public/locales/en/sections.json) :
json
{
"myNewSection": {
"title": "My New Title",
"description": "Description in English"
}
}3. Utiliser dans un Composant
astro
---
import { t } from "@/utils/i18n";
const { locale } = Astro.props;
const title = t("sections:myNewSection.title", { lng: locale });
const description = t("sections:myNewSection.description", { lng: locale });
---
<h2>{title}</h2>
<p>{description}</p>Bonnes Pratiques
✅ Toujours définir dans les 2 langues
json
// ✅ Bon : FR et EN synchronisés
// fr/sections.json
{ "hero": { "title": "Héros" } }
// en/sections.json
{ "hero": { "title": "Hero" } }json
// ❌ Mauvais : Manque EN
// fr/sections.json
{ "hero": { "title": "Héros" } }
// en/sections.json
{ } // Vide !✅ Utiliser des clés descriptives
json
// ✅ Bon : Clés explicites
{
"experience": {
"showMore": "Voir plus",
"showLess": "Voir moins"
}
}
// ❌ Mauvais : Clés génériques
{
"btn1": "Voir plus",
"btn2": "Voir moins"
}✅ Grouper logiquement
json
// ✅ Bon : Regroupement cohérent
{
"hero": {
"showProfile": "...",
"phoneTitle": "...",
"mailTitle": "..."
}
}
// ❌ Mauvais : Tout au même niveau
{
"heroShowProfile": "...",
"heroPhoneTitle": "...",
"heroMailTitle": "..."
}✅ Nommer explicitement les variables
json
// ✅ Bon : Variables explicites
"phoneTitle": "Contacter {{name}} au {{phone}}"
// ❌ Mauvais : Variables génériques
"phoneTitle": "Contacter {{var1}} au {{var2}}"Debugging
Tester une Traduction
astro
---
const test = t("sections:hero.showProfile", {
lng: "fr",
name: "Simon"
});
console.log(test);
// → "Voir le profil de Simon sur" (si tout va bien)
// → "sections:hero.showProfile" (si erreur)
---Vérifier les Fichiers JSON
bash
# Valider la syntaxe
cat public/locales/fr/sections.json | jq .
# Afficher le contenu
jq . public/locales/fr/sections.jsonVérifier le Cache
En DEV, le cache peut poser problème si on modifie les JSON sans recharger.
Solution : Recharger la page complètement (Cmd+R).