Internationalisation avec next-intl : traductions FR/EN, conventions de cles et workflow d'ajout de nouvelles traductions.
Speedcube Master utilise next-intl pour l'internationalisation. Le francais est la locale par defaut. Les fichiers de traduction sont situes dans le dossier messages/ a la racine du projet.
messages/fr.jsonTraductions francaises (par defaut)
messages/en.jsonTraductions anglaises
// messages/fr.json (extrait)
{
"timer": {
"title": "Timer",
"inspection": "Inspection",
"ready": "Pret",
"solving": "Resolution en cours...",
"delete_confirm": "Supprimer cette solve ?",
"session": "Session",
"new_session": "Nouvelle session",
"best": "Meilleur",
"average": "Moyenne",
"ao5": "Ao5",
"ao12": "Ao12"
},
"algos": {
"title": "Algorithmes",
"search": "Rechercher un algorithme...",
"method": "Methode",
"category": "Categorie",
"no_results": "Aucun algorithme trouve",
"contribute": "Contribuer"
},
"common": {
"save": "Enregistrer",
"cancel": "Annuler",
"delete": "Supprimer",
"loading": "Chargement...",
"error": "Une erreur est survenue",
"retry": "Reessayer"
}
}
// messages/en.json (extrait)
{
"timer": {
"title": "Timer",
"inspection": "Inspection",
"ready": "Ready",
"solving": "Solving...",
"delete_confirm": "Delete this solve?",
"session": "Session",
"new_session": "New session",
"best": "Best",
"average": "Average",
"ao5": "Ao5",
"ao12": "Ao12"
},
"algos": {
"title": "Algorithms",
"search": "Search an algorithm...",
"method": "Method",
"category": "Category",
"no_results": "No algorithm found",
"contribute": "Contribute"
},
"common": {
"save": "Save",
"cancel": "Cancel",
"delete": "Delete",
"loading": "Loading...",
"error": "An error occurred",
"retry": "Retry"
}
}Le hook useTranslations() de next-intl fournit les traductions pour un namespace donne. Il doit etre appele dans un Client Component.
"use client";
import { useTranslations } from "next-intl";
// Utilisation basique avec namespace
function TimerHeader() {
const t = useTranslations("timer");
return (
<div>
<h1 className="text-2xl font-bold">{t("title")}</h1>
<p className="text-muted-foreground">{t("inspection")}</p>
</div>
);
}
// Utilisation avec plusieurs namespaces
function SolveCard({ solve }: { solve: Solve }) {
const t = useTranslations("timer");
const tCommon = useTranslations("common");
return (
<Card>
<CardContent>
<p>{formatTime(solve.time)}</p>
<div className="flex gap-2">
<Button onClick={handleSave}>{tCommon("save")}</Button>
<Button variant="destructive" onClick={handleDelete}>
{tCommon("delete")}
</Button>
</div>
</CardContent>
</Card>
);
}
// Utilisation avec interpolation
// fr.json: "welcome": "Bienvenue, {name} !"
function WelcomeBanner({ name }: { name: string }) {
const t = useTranslations("common");
return <p>{t("welcome", { name })}</p>;
}
// Utilisation avec pluralisation
// fr.json: "solve_count": "{count, plural, =0 {Aucune solve} one {1 solve} other {# solves}}"
function SolveCount({ count }: { count: number }) {
const t = useTranslations("timer");
return <p>{t("solve_count", { count })}</p>;
}Les cles de traduction suivent le format namespace.key. Le namespace correspond generalement a la page ou au domaine fonctionnel.
| Namespace | Exemples de cles |
|---|---|
timer | timer.inspection, timer.ready, timer.ao5 |
algos | algos.search, algos.method, algos.contribute |
common | common.save, common.cancel, common.error |
dashboard | dashboard.stats, dashboard.recent_solves |
room | room.create, room.join, room.waiting |
auth | auth.sign_in, auth.sign_out, auth.profile |
delete_confirmcommon pour les textes reutilises partout (save, cancel, error...)commonWorkflow pour ajouter une nouvelle cle de traduction a l'application.
Ajouter la cle dans fr.json
Ajouter la cle dans le namespace correspondant du fichier messages/fr.json.
Ajouter la cle dans en.json
Ajouter la meme cle avec la traduction anglaise dans messages/en.json.
Utiliser dans le composant
Appeler useTranslations("namespace") puis t("key") dans le composant.
Verifier les deux langues
Tester manuellement en basculant la langue pour s'assurer que les deux traductions s'affichent correctement.
// Etape 1 : Ajouter dans messages/fr.json
{
"learning": {
"blind": {
"title": "Apprentissage Blind",
"memo_phase": "Phase de memorisation",
"exec_phase": "Phase d'execution"
}
}
}
// Etape 2 : Ajouter dans messages/en.json
{
"learning": {
"blind": {
"title": "Blind Learning",
"memo_phase": "Memorization phase",
"exec_phase": "Execution phase"
}
}
}
// Etape 3 : Utiliser dans le composant
"use client";
import { useTranslations } from "next-intl";
function BlindLearningHeader() {
const t = useTranslations("learning.blind");
return (
<div>
<h1>{t("title")}</h1>
<div className="flex gap-4">
<span>{t("memo_phase")}</span>
<span>{t("exec_phase")}</span>
</div>
</div>
);
}La locale est detectee automatiquement selon un ordre de priorite. L'utilisateur peut aussi changer la langue manuellement via le selecteur de langue dans le header.
localStorage
Si l'utilisateur a deja choisi une langue, elle est sauvegardee dans localStorage.
Langue du navigateur
Detectee via navigator.language. Si elle commence par "fr", le francais est utilise ; sinon l'anglais.
Defaut : francais (fr)
Si aucune preference n'est trouvee, le francais est utilise par defaut.
// Logique de detection de locale
function getLocale(): "fr" | "en" {
// 1. Verifier localStorage
const saved = localStorage.getItem("locale");
if (saved === "fr" || saved === "en") return saved;
// 2. Verifier la langue du navigateur
const browserLang = navigator.language;
if (browserLang.startsWith("fr")) return "fr";
// 3. Defaut
return "fr";
}
// Changement de langue avec persistance
function useLocaleSwitch() {
const [locale, setLocale] = useState<"fr" | "en">(getLocale());
const switchLocale = (newLocale: "fr" | "en") => {
localStorage.setItem("locale", newLocale);
setLocale(newLocale);
// Recharger pour appliquer la nouvelle locale
window.location.reload();
};
return { locale, switchLocale };
}
// Selecteur de langue dans le header
function LanguageSelector() {
const { locale, switchLocale } = useLocaleSwitch();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm">
{locale === "fr" ? "FR" : "EN"}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem onClick={() => switchLocale("fr")}>
Francais
</DropdownMenuItem>
<DropdownMenuItem onClick={() => switchLocale("en")}>
English
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}