- Publié le
Internationalisation de la V 2.0
- Auteurs
- Nom
- Tails Azimuth
NOTE IMPORTANTE:
Il s'agit toujours d'un WIP (Work In Progress).
J'ai récemment modifié toute la logique complexe du serveur pour la pagination et les balises, avec l'utilisation de la bibliothèque Zustand, et j'ai tout déplacé vers deux composants (pagination et ListLayout). Bien que ce ne soit pas optimal à des fins de référencement, l'objectif de ces modifications est de trouver une solution pour gérer toutes les URL de manière plus efficace (les URL traduites sont très mauvaises pour le référencement). De plus, je ne pense pas que l'impact de ces premiers changements soit vraiment mauvais pour le référencement, car la plupart des utilisateurs utiliseront ce référentiel et ce code pour leur blog de développement personnel.
Donc pour l'instant, la logique des pages de blog et des balises est entièrement gérée côté client (ce qui se traduit également par une interface utilisateur plus rapide pour les utilisateurs). Cela signifie également un code et des pages côté serveur moins complexes.
sitemap.xml et robots.txt sont désormais parfaitement gérés. J'ai effectué toutes les modifications nécessaires, la prochaine étape consiste à corriger le flux RSS.
Comment pouvez-VOUS aider? Vous pouvez aider ce projet en faisant des relations publiques. J'ai besoin d'aide pour optimiser les performances sur mobile
Introduction
Dans cet article, nous discuterons de l'implémentation de i18n et de ce que cela change en comparaison de la version V.2 originale. Pour une meilleure compréhension des fonctionnalités de base, il vous faudra consulter les autres articles, ou la documentation originelle sur le github de timlrx
Vous utilisez le dépôt? Faites-le-moi savoir et je créerai une liste si vous souhaitez que votre propre blog y soit répertorié.
Si vous trouvez ce projet utile, veuillez lui attribuer un ⭐ pour montrer votre soutien.
Motivation
Le modèle original est incroyable, et tous les crédits majeurs pour celui-ci reviennent à son créateur @Timlrx. J'utilisais la V.1 de la version i18n, qui était plutôt compliquée à utiliser et que le créateur semble avoir malheureusement abandonnée. C'est donc ma participation et mon don à la communauté !
De plus, je suis en train de refaire mon propre site web, qui utilise la page du routeur, et une partie du code pour le blog internationalisé de la V.1. Je voulais migrer vers l'application router, mais pour cela, j'ai d'abord dû apprendre à internationaliser un site avec l'application router, j'ai donc pris ce dépôt comme formation.
J'adore l'idée d'aider d'autres développeurs à commencer rapidement à partager leurs précieuses connaissances avec le monde entier, en améliorant Internet, que ce soit dans leur langue maternelle ou en anglais 😌
J'ai également conçu un modèle beaucoup plus complet pour les artistes, les créateurs de contenu et les développeurs, que j'utilise pour mon propre site et qui est disponible ici :
Version normale :
Version I18 :
Mon propre site Web basé sur ce nouveau modèle :
Changements:
Nouvelles fonctionnalités
Ce dépôt sera parfois mis à jour avec de nouvelles fonctionnalités (non présentes dans le dépôt d'origine) Tout cela peut parfois sembler obsessionnel concernant l'interface et les détails, -peut-être trop soigné ou un peu surfait- mais j'utilise aussi ce dépôt comme un espace de jeu et d'apprentissage.
Pour l'instant :
Nouveau composant "sidetoc" : affiche automatiquement la table des matières de vos articles dans une sidebar dédiée.
Intégration de l'email, du thème, ainsi qu'un bouton pour copier rapidement l'URL de la page sur laquelle vous vous trouvez, avec la commande kbar palette. La motivation pour cela est d'avoir exploré d'autres bibliothèques de palettes de commandes, certaines proposant des éléments imbriqués pour les 'Actions'. Malheureusement ce n'est pas possible avec kbar, mais cela m'a donné de nouvelles idées !
Fonctionnalité multi-auteurs pour la section "à propos" : chaque auteur peut avoir sa propre page à propos disponible dans un menu déroulant sur les grands écrans, ou affichée directement sur les petits écrans. Si vous souhaitez la désactiver et n'utiliser que la section à propos "normale", classique, allez sur sitemetadata.js et définissez multiauthors sur false. Dans tous les cas, votre auteur principal doit maintenant avoir le champ "default" défini sur true.
Section Featured sur la page d'accueil pour les articles que vous souhaitez épingler en haut : définissez featured sur true (max deux articles par défaut, peut être modifié dans le fichier Featured.tsx, dans le dossier component) Le programme choisira les deux derniers articles avec "featured : true". Si aucun article en vedette n'est disponible, cette section ne sera tout simplement pas affichée !
Chaque tag a maintenant sa propre pagination ! Si le nombre d'articles est supérieur à celui que vous avez défini (par défaut, défini sur 5) alors une nouvelle page est automatiquement créée pour les articles suivants incluant le même tag.
Le système de commentaires Waline est maintenant pris en charge ! C'est probablement le meilleur système de commentaires open source du moment, avec même i18n et de nombreuses autres fonctionnalités intéressantes ! Tout d'abord, suivez le tutoriel officiel pour configurer la base de données de commentaires et votre serveur vercel. Il existe de nombreuses options disponibles, alors prenez le temps de lire leur documentation. Comme il est basé sur Vue, il est toujours compatible avec Next.js, et j'ai créé un nouveau composant de commentaire. Vous le trouverez dans le dossier "walinecomponents". J'ai également ajouté un nouveau fichier css, et vous pouvez modifier le style ici si nécessaire. Une fois que vous avez déployé votre application pour les commentaires, modifiez le fichier sitemetadata.js. Définissez la propriété "iscomments" sur false, définissez "iswaline" sur true et définissez l'URL de votre serveur de commentaires en conséquence dans "walineServer". Si votre langue n'est pas prise en charge par waline, faites une demande de publication sur leur référentiel ou demandez-leur gentiment d'ajouter votre propre traduction (fournissez-la vous-même au préalable). C'est ce que j'ai fait pour soutenir le français, et c'est ainsi que nous travaillons dans le monde open source !
Les séries pour vos articles sont désormais prises en charge, consultez la démo déployée!
Exemple d'intégration de cette nouvelle fonctionnalité:
title: Internationalization of V 2.0
series:
order: 4 // Vous devez ajouter un numéro pour l'ordre réel de votre publication dans votre série d'articles.
title: "Blog Starter" // Vous devez ajouter le même titre à tous vos articles de la même série
date: '2023-11-17'
lastmod: '2023-11-17'
language: en
tags: ['next-js', 'tailwind', 'guide', 'features', 'i18n']
authors: ['default']
images: ['/static/images/twitter-card.png']
draft: false
summary: Presentation of the Starter Blog Tailwind Next-js v2.0, with addition and support for multiple languages.
Composant de partage : vous ou vos utilisateurs pouvez partager vos articles de blog sur Facebook, Twitter, Linkedin, WhatsApp, Threads ou Telegram en toute simplicité ! Que serait un blog moderne de 2024 sans cette possibilité ?
Transitions de page fluides grâce à Framer Motion (consultez le fichier template.tsx dans le dossier app et jetez un œil à la documentation next.js suivante pour la fonctionnalité de fichier template) Remarque : il s'agit d'une implémentation basique mais efficace. Je vous encourage vivement à expérimenter framer-motion et son utilisation au sein du nouveau routeur. J'ai également ajouté un peu de Framer Motion à la fenêtre de contact de formspree et au composant ListLayoutWithTags.tsx
Nouveau composant MDX : excellent lecteur audio pour les fichiers mdx (au cas où vous feriez des podcasts, ou même de la musique), grâce à react-h5-audio-player
Indicateur de taille d'écran Tailwind : une petite aide pour le mode développement et le responsive design (voir TwSizeIndicator.tsx dans /components/helper)
Formspree prise en charge de l'icône mail, avec une belle boîte de dialogue modale. Formspree permet à vos utilisateurs de vous contacter et de vous envoyer des messages directement depuis votre site, avec une protection anti-spam. Créez simplement un compte de base gratuit, lisez la documentation et récupérez la clé de votre compte formspree, puis remplacez la clé par la vôtre ici, dans components/formspree/index.tsx :
/* Ligne 11*/
const [state, handleSubmit, reset] = useForm('xdojkndq')
NOTE IMPORTANTE : vous devez remplacer la clé dans useform comme ceci : useform('[votre clé]'). La clé fournie est une clé de test que j'ai fournie. Vous pouvez l'utiliser pour vérifier si la boîte toast est fonctionnelle par exemple, mais sachez que je recevrai tous vos messages de test. (Vous pouvez toujours m'envoyer un message de bienvenue amical !)
Si vous ne souhaitez pas utiliser Formspree, accédez au fichier siteMetadata.js et définissez formspree sur "false".
Librairies
Pour les traductions, la bibliothèque choisie n'est pas next-translate comme dans la V.1 de GautierArcin, mais les bibliothèques suivantes :
- i18next
- i18next-browser-languagedetector
- i18next-resources-to-backend
- React-i18next
En effet, avec la nouvelle version de next-js et l'app router, il m'a été plus facile de trouver des informations et des tutoriels pour que tout fonctionne comme prévu. (J'ai d'abord essayé avec next-translate, mais il y a trop de problèmes non résolus actuellement avec cette bibliothèque et les fonctionnalités liées au nouveau router)
Configuration
Au sein du dossier app, tout le contenu a été déplaçé vers un nouveau dossier [locale]" : ceci est la manière officielle recommandée par next.js. A également été ajouté un dossier i18n:
app
│
[locale]
│
├── i18n
│ │
│ ├──locales
│ │ │
│ │ ├── en
│ │ │ ├── about.json
│ │ │ │
│ │ │ ├── home.json
│ │ │ │
│ │ │ └── ...
│ │ └── fr
│ │ ├── about.json
│ │ │
│ │ ├── home.json
│ │ │
│ │ └── ...
│ │
│ │
│ ├── client.ts
│ ├── locales.js
│ ├── server.ts
│ └── settings.ts
│
└── ...
C'est donc dans ce dossier i18n que se situe la logique principale pour l'internationalisation de l'application.
- Fichiers json :
Le sous-dossier "locales" contient les fichiers .json où vous définirez vos traductions, la convention étant de définir un fichier par page de votre site, avec le nom de la page concernée pour le nom du fichier json. Il y a également un fichier "common" : si vous ne spécifiez pas de "namespace" ou ns (le nom du fichier sans l'extension json) dans vos pages ou composants, les traductions seront piochées dans ce fichier par défaut.
*Important : pour chaque langue, il doit y avoir un fichier correspondant avec le même nom, par exemple un fichier "about" pour "fr" et pour "en", etc. Ainsi que des clefs de traduction avec le même nom au sein de chaque fichier.
Exemple :
En anglais dans le dossier "en":
{
"title": "Projects",
"description": "Showcase your projects with a hero image (16 x 9)",
"learn": "Learn more",
"subtitle": "Here you will find information about my current projects",
"linkto": "Link to"
}
En français dans le dossier "fr":
{
"title": "Projets",
"description": "Présentez vos projets avec une image (16 x 9)",
"learn": "En savoir plus",
"subtitle": "Ici vous trouverez des informations sur mes projets actuels.",
"linkto": "Lien vers"
}
- locales.js :
Il s'agit du fichier où vous définirez les langues que vous souhaitez utiliser, ainsi que la langue par défaut:
const fallbackLng = 'en' // langue par défaut
const secondLng = 'fr'
module.exports = { fallbackLng, secondLng }
Vous pouvez ajouter autant de langues que souhaité:
/* Exemple d'ajout d'une 3ème langue :*/
const fallbackLng = 'en'
const secondLng = 'fr'
const thirdLng = 'es'
module.exports = { fallbackLng, secondLng, thirdLng }
Toutefois, cela nécessitera quelques étapes de configuration supplémentaires au sein d'autres fichiers (principalement des fichiers qui sont discutés dans cet article)
Vous pouvez également intervertir la langue par défaut et la seconde langue :
/* Exemple de modification de langue par défaut:*/
const fallbackLng = 'fr'
const secondLng = 'en'
module.exports = { fallbackLng, secondLng}
- settings.ts
Il s'agit d'un fichier de configuration, qui permet de définir un objet locales ainsi que les options correspondantes :
import type { InitOptions } from 'i18next'
import { fallbackLng, secondLng } from './locales'
/* Objet locales, qui définit toutes les langues qui seront utilisées dans l'application: */
export const locales = [fallbackLng, secondLng] as const
/* Définition typescript de type pour nos locales :*/
export type LocaleTypes = (typeof locales)[number]
/* "Namespace" (ou ns) par défaut : les traductions seront piochées dans le fichier
common.json si aucun ns n'est spécifié dans vos composants ou pages: */
export const defaultNS = 'common'
/* Fonction qui sera réutilisée dans les fichiers client.ts et server.ts: */
export function getOptions(locale = fallbackLng, ns = defaultNS): InitOptions {
return {
debug: true,
supportedLngs: locales,
fallbackLng,
lng: locale,
fallbackNS: defaultNS,
defaultNS,
ns,
}
}
- client.ts et server.ts :
Sans rentrer dans des détails complexes, ces deux fichiers exportent chacun une fonction pour la traduction (useTranslation côté client, createTranslation côté serveur), réutilisables dans vos pages et composants :
export function useTranslation(lng: LocaleTypes, ns: string) {
const translator = useTransAlias(ns)
const { i18n } = translator
/* Exécuté lorsque le contenu est rendu côté serveur: */
if (runsOnServerSide && lng && i18n.resolvedLanguage !== lng) {
i18n.changeLanguage(lng)
} else {
/* Utiliser notre implémentation personnalisée lors de l'exécution côté client: */
// eslint-disable-next-line react-hooks/rules-of-hooks
useCustomTranslationImplem(i18n, lng)
}
return translator
}
export async function createTranslation(lang: LocaleTypes, ns: string) {
const i18nextInstance = await initI18next(lang, ns)
return {
/* La fonction de traduction "t" que nous utiliserons dans nos composants: */
// e.g. t('greeting')
t: i18nextInstance.getFixedT(lang, Array.isArray(ns) ? ns[0] : ns),
}
}
Exemple de composant côté client, avec traduction de l'aria-label du bouton :
'use client'
import { useEffect, useState } from 'react'
import { useTheme } from 'next-themes'
/*Importation du hook fourni par next.js pour récupérer la langue
définie par l'utilisateur, et de la fonction de traduction côté client: */
import { useParams } from 'next/navigation'
import { LocaleTypes } from 'app/[locale]/i18n/settings'
import { useTranslation } from 'app/[locale]/i18n/client'
const ThemeSwitch = () => {
/* Utilisation du hook fourni par next.js pour récupérer la langue actuellement définie: */
const locale = useParams()?.locale as LocaleTypes
/* Utilisation de la fonction de traduction côté client:
pas de namespace (ns) défini (crochets vide), par conséquent la traduction sera piochée
dans le fichier common.json */
const { t } = useTranslation(locale, '')
const [mounted, setMounted] = useState(false)
const { theme, setTheme, resolvedTheme } = useTheme()
useEffect(() => setMounted(true), [])
if (!mounted) {
return null
}
return (
<button
/* Traduction de l'aria-label */
aria-label={t('darkmode')}
onClick={() => setTheme(theme === 'dark' || resolvedTheme === 'dark' ? 'light' : 'dark')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
className="h-6 w-6 text-gray-900 dark:text-gray-100"
>
{mounted && (theme === 'dark' || resolvedTheme === 'dark') ? (
<path
fillRule="evenodd"
d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"
clipRule="evenodd"
/>
) : (
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" />
)}
</svg>
</button>
)
}
export default ThemeSwitch
Exemple de composant côté serveur :
import Link from './Link'
import siteMetadata from '@/data/siteMetadata'
import SocialIcon from '@/components/social-icons'
/*Importation de la fonction de traduction côté serveur: */
import { createTranslation } from 'app/[locale]/i18n/server'
import { LocaleTypes } from 'app/[locale]/i18n/settings'
type Props = {
params: { locale: LocaleTypes }
}
/* la langue actuelle est passée en tant que prop des paramètres de la page: */
export default async function Footer({ params: { locale } }: Props) {
/* Utilisation de la fonction de traduction côté serveur, avec le namespace "footer" */
const { t } = await createTranslation(locale, 'footer')
return (
<footer>
<div className="mt-16 flex flex-col items-center">
<div className="mb-3 flex space-x-4">
<SocialIcon kind="mail" href={`mailto:${siteMetadata.email}`} size={6} />
<SocialIcon kind="github" href={siteMetadata.github} size={6} />
<SocialIcon kind="facebook" href={siteMetadata.facebook} size={6} />
<SocialIcon kind="youtube" href={siteMetadata.youtube} size={6} />
<SocialIcon kind="linkedin" href={siteMetadata.linkedin} size={6} />
<SocialIcon kind="twitter" href={siteMetadata.twitter} size={6} />
</div>
<div className="mb-2 flex space-x-2 text-sm text-gray-500 dark:text-gray-400">
<div>{siteMetadata.author}</div>
<div>{` • `}</div>
<div>{`© ${new Date().getFullYear()}`}</div>
<div>{` • `}</div>
<Link href="/">{siteMetadata.title}</Link>
</div>
<div className="mb-8 text-sm text-gray-500 dark:text-gray-400">
<Link href="https://github.com/timlrx/tailwind-nextjs-starter-blog">
{t('theme')}
</Link>
</div>
</div>
</footer>
)
}
Pour la création de nouveaux composants ou pages, vous devrez donc vous appuyer sur ces deux fonctions concernant vos traductions, selon que le composant soit rendu côté client, ou côté serveur.
- Middleware.ts :
I18n n'étant pas supporté nativement au sein du nouveau router, il s'agit d'un fichier essentiel au bon fonctionnement de l'ensemble. Il est également essentiel d'utiliser le "matcher" (ici avec des valeurs inversées qui permettent d'exclure les dossiers et fichiers qui ne doivent pas être pris en charge par le middleware)
import { NextResponse, NextRequest } from 'next/server'
import { locales } from 'app/[locale]/i18n/settings'
import { fallbackLng } from 'app/[locale]/i18n/locales'
export function middleware(request: NextRequest) {
/* Vérifier si une langue est prise en charge dans le nom de chemin: */
const pathname = request.nextUrl.pathname
/* Vérifier si la langue par défaut est dans le nom de chemin: */
if (pathname.startsWith(`/${fallbackLng}/`) || pathname === `/${fallbackLng}`) {
/* ex: la requête entrante est: /en/about
Le nouveau nom de chemin est maintenant: /about */
return NextResponse.redirect(
new URL(
pathname.replace(`/${fallbackLng}`, pathname === `/${fallbackLng}` ? '/' : ''),
request.url
)
)
}
const pathnameIsMissingLocale = locales.every(
(locale) => !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`
)
if (pathnameIsMissingLocale) {
/* Nous sommes sur la langue par défaut
Réecriture pour que next.js comprenne */
// ex: la requête entrante est: /about
// Informer Next.js qu'il devrait se comporter comme si c'était: /en/about
return NextResponse.rewrite(new URL(`/${fallbackLng}${pathname}`, request.url))
}
}
export const config = {
/* Ne pas executer le middleware sur les chemins suivants: */
// prettier-ignore
matcher:
'/((?!api|static|data|css|scripts|.*\\..*|_next).*|sitemap.xml)',
}
Articles
Tous les articles sont regroupés au sein du dossier "data/blog"
Ils sont organisés en sous-dossiers : « data/blog/en » pour l'anglais, et « data/blog/fr » pour le français. Vous pouvez nommer les sous-dossiers pour vos langues comme vous le souhaitez : avec la configuration actuelle dans le fichier contentlayer.config.ts, le slug de vos articles est le nom de fichier de votre article, sans l'extension .mdx. Tous vos articles traduits doivent avoir le même nom que les originaux, pour des raisons de gestion de la traduction.
Exemple :
En : « data/blog/en/code-sample.mdx » Fr : « data/blog/fr/code-sample.mdx »
Etc.
- En-têtes de vos articles :
---
title: titre de l'article
date: date de création
lastmod: dernière date de modification
language: langue de l'article
tags: balises
authors: auteurs
images: images
draft: en construction ou non
summary: résumé
---
- contentlayer.config.ts :
Au sein du fichier "contentlayer.config.ts" il y a donc des changements mineurs dûs à l'internationalisation :
export const Blog = defineDocumentType(() => ({
name: 'Blog',
filePathPattern: 'blog/**/*.mdx',
contentType: 'mdx',
fields: {
title: { type: 'string', required: true },
series: { type: 'nested', of: Series },
date: { type: 'date', required: true },
language: { type: 'string', required: true }, // Nouveau champs obligatoire
tags: { type: 'list', of: { type: 'string' }, default: [] },
lastmod: { type: 'date' },
draft: { type: 'boolean' },
summary: { type: 'string' },
images: { type: 'json' },
authors: { type: 'list', of: { type: 'string' }, required: true },
layout: { type: 'string' },
bibliography: { type: 'string' },
canonicalUrl: { type: 'string' },
},
...
)})
Pour le champs "auteurs" :
export const Authors = defineDocumentType(() => ({
name: 'Authors',
filePathPattern: 'authors/**/*.mdx',
contentType: 'mdx',
fields: {
name: { type: 'string', required: true },
language: { type: 'string', required: true }, // Nouveau champs obligatoire
default: {type: 'boolean'},
avatar: { type: 'string' },
occupation: { type: 'string' },
company: { type: 'string' },
email: { type: 'string' },
twitter: { type: 'string' },
linkedin: { type: 'string' },
github: { type: 'string' },
layout: { type: 'string' },
},
-Génération des tags :
Là aussi, il a été nécessaire de faire des modifications, afin de générer un objet .json avec des tags pour chaque langue.
function createTagCount(allBlogs) {
const tagCount = {
[fallbackLng]: {},
[secondLng]: {},
}
allBlogs.forEach((file) => {
if (file.tags && (!isProduction || file.draft !== true)) {
file.tags.forEach((tag: string) => {
const formattedTag = GithubSlugger.slug(tag)
if (file.language === fallbackLng) { // tags pour la langue par défaut
tagCount[fallbackLng][formattedTag] = (tagCount[fallbackLng][formattedTag] || 0) + 1
} else if (file.language === secondLng) { // tags pour la seconde langue
tagCount[secondLng][formattedTag] = (tagCount[secondLng][formattedTag] || 0) + 1
}
})
}
})
writeFileSync('./app/[locale]/tag-data.json', JSON.stringify(tagCount))
}
Note : si vous souhaitez ajouter d'autres langues (3, 4 ou même 5 langues), il vous faudra modifier la logique pour prendre en charge ces nouvelles langues.
Auteurs
Les dossiers contenant les auteurs sont organisés par langue, et les informations sur les auteurs peuvent être traduites.
L'implémentation est assez simple et directe : si vous voulez modifier ou ajouter une langue, modifiez ou ajoutez simplement les dossiers avec vos traductions correspondantes pour les nouvelles langues.
Fichier siteMetadata et nouveau fichier localeMetadata
Le fichier siteMetadata.js présent dans le dossier "/data" ne nécessite pas de modifications liées à l'internationalisation.
Par contre, afin de gérer au mieux les métadonnées, il a fallu créer un nouveau fichier, pour le titre et la description :
type Metadata = {
[locale: string]: string
}
/* Ajoutez ou modifiez le titre ici selon les langues choisies: */
export const maintitle: Metadata = {
en: 'Next.js i18n Starter Blog',
fr: 'Starter Blog Next.js i18n',
}
/* Ajoutez ou modifiez la description ici selon les langues choisies: */
export const maindescription: Metadata = {
en: 'A blog created with Next.js, i18n and Tailwind.css',
fr: 'Un blog crée avec tailwind, i18n et next.js',
}
Onglet "Projets"
La logique nécessaire à l'onglet "projets" réside dans le fichier suivant, également présent dans le dossier"/data" :
type Project = {
title: string
description: string
imgSrc: string
href: string
}
type ProjectsData = {
[locale: string]: Project[]
}
const projectsData: ProjectsData = {
en: [
{
title: 'A Search Engine',
description: `What if you could look up any information in the world? Webpages, images, videos
and more. Google has many features to help you find exactly what you're looking
for.`,
imgSrc: '/static/images/google.png',
href: 'https://www.google.com',
},
{
title: 'The Time Machine',
description: `Imagine being able to travel back in time or to the future. Simple turn the knob
to the desired date and press "Go". No more worrying about lost keys or
forgotten headphones with this simple yet affordable solution.`,
imgSrc: '/static/images/time-machine.jpg',
href: '/blog/the-time-machine',
},
],
fr: [
{
title: 'Un moteur de recherche',
description: `Et si vous pouviez rechercher n'importe quelle information dans le monde ? Pages Web, images, vidéos
et plus. Google propose de nombreuses fonctionnalités pour vous aider à trouver exactement ce que vous cherchez.`,
imgSrc: '/static/images/google.png',
href: 'https://www.google.com',
},
{
title: 'La Machine à remonter le temps',
description: `Imaginez pouvoir voyager dans le temps ou vers le futur. Tournez simplement le bouton
à la date souhaitée et appuyez sur "Go". Ne vous inquiétez plus des clés perdues ou
écouteurs oubliés avec cette solution simple mais abordable.`,
imgSrc: '/static/images/time-machine.jpg',
href: '/blog/the-time-machine',
},
],
}
export default projectsData
Encore une fois, modifiez simplement la logique en conservant la même structure générale, et selon vos langues choisies/et/ou nombre de langues.
Barre de recherche :
Le dépôt original permets le support de kbar et d'algolia.
Ici, la barre de recherche repose sur la librairie kbar, et le support d'Algolia n'est pas prévu. Si vous préferez utiliser Algolia, ce sera donc à vous de l'implémenter sur votre site, à la place de kbar.
Il y a un problème lors de l'utilisation de traductions standard, j'ai donc mis en place une solution de contournement pour ce problème. Modifiez simplement le nom de chaque élément de menu, ainsi que l'objet navigationSection, en fonction des langues que vous utilisez.
export const SearchProvider = ({ children }: SearchProviderProps) => {
const locale = useParams()?.locale as LocaleTypes
const { t } = useTranslation(locale, '')
const router = useRouter()
const authors = allAuthors
.filter((a) => a.language === locale)
.sort((a, b) => (a.default === b.default ? 0 : a.default ? -1 : 1)) as Authors[]
const authorSearchItems = authors.map((author) => {
const { name, slug } = author
return {
id: slug,
name: name,
keywords: '',
shortcut: [],
section: locale === fallbackLng ? 'Authors' : 'Auteurs',
perform: () => router.push(`/${locale}/about/${slug}`),
icon: (
<i>
<AboutIcon />
</i>
),
}
})
const showAuthorsSearch = siteMetadata.multiauthors
const authorsActions = [
...(showAuthorsSearch ? authorSearchItems : []),
...(showAuthorsSearch
? []
: [
{
id: 'about',
name: locale === fallbackLng ? 'About' : 'À propos',
keywords: '',
shortcut: ['a'],
section: locale === fallbackLng ? 'Navigate' : 'Naviguer',
perform: () => router.push(`/${locale}/about`),
icon: (
<i>
<AboutIcon />
</i>
),
},
]),
]
/* problème lors de l'utilisation de traductions régulières, il s'agit d'une solution de contournement pour montrer comment implémenter les titres de section */
/*Modifiez la ligne suivante en fonction de vos langues implémentées : */
const navigationSection = locale === fallbackLng ? 'Navigate' : 'Naviguer'
return (
<KBarSearchProvider
kbarConfig={{
searchDocumentsPath: 'search.json',
/*problème lors de l'utilisation de traductions régulières, il s'agit d'une solution de contournement pour montrer comment implémenter les titres de menu traduits */
defaultActions: [
{
id: 'home',
name: locale === fallbackLng ? 'Home' : 'Accueil',
keywords: '',
shortcut: ['h'],
section: navigationSection,
perform: () => router.push(`/${locale}`),
icon: (
<i>
<HomeIcon />
</i>
),
},
{
id: 'blog',
name: locale === fallbackLng ? 'Blog' : 'Blog',
keywords: '',
shortcut: ['b'],
section: navigationSection,
perform: () => router.push(`/${locale}/blog`),
icon: (
<i>
<BlogIcon />
</i>
),
},
{
id: 'tags',
name: locale === fallbackLng ? 'Tags' : 'Tags',
keywords: '',
shortcut: ['t'],
section: navigationSection,
perform: () => router.push(`/${locale}/tags`),
icon: (
<i>
<TagsIcon />
</i>
),
},
{
id: 'projects',
name: locale === fallbackLng ? 'Projects' : 'Projets',
keywords: '',
shortcut: ['p'],
section: navigationSection,
perform: () => router.push(`/${locale}/projects`),
icon: (
<i>
<ProjectsIcon />
</i>
),
},
...authorsActions,
],
onSearchDocumentsLoad(json) {
return json
.filter((post: CoreContent<Blog>) => post.language === locale)
.map((post: CoreContent<Blog>) => ({
id: post.path,
name: post.title,
keywords: post?.summary || '',
section: t('content'),
subtitle: post.tags.join(', '),
perform: () => router.push(`/${locale}/blog/${post.slug}`),
}))
},
}}
>
{children}
</KBarSearchProvider>
)
}
Choses à faire :
Corriger la traduction dans la page 404. Ceci est lié au fonctionnement actuel de la fonction not-found, il faut donc attendre un correctif du côté de next-js voir ici : i18n pour la page not-found
Corriger rss.mjs. Si vous trouvez une solution de votre côté, n'hésitez pas à ouvrir un PR !
Tout le reste fonctionne actuellement comme prévu.
Voici une autre solution possible pour l'intégration i18n concernant le SEO, et même l'URL traduite :
Toute aide pour des améliorations et/ou des rapports de bugs est la bienvenue !
Notes importantes :
J'utilise un composant Link personnalisé pour la sélection de la langue : je préfère cela à l'élément de sélection HTML (plus facile à personnaliser). Le petit inconvénient est qu'il nécessite plus de code. Si vous préférez, vous êtes libre d'adapter et d'utiliser l'élément select à la place, mais je Je le garderai tel quel pour le modèle.
Ne mettez pas à jour les dépendances : cela cassera votre application car certaines choses doivent être corrigées du côté de ces bibliothèques Auteur : pxlsyl