Mises à jour
Nouveautés
Historique des versions et des fonctionnalités de 3amia · AI.
v1.22.11 — Juillet 2026 — FIX CRITIQUE (prod figée)
- ·Boucle infinie sur
/articles/[slug]— introduite en v1.22.10 (commitbd041f0) — ouvrir une fiche article gelait l'onglet (boucle infinie sur le thread principal). Cause confirmée :INLINE_RE, la regex/gdu tokenizer inline, était déclarée au niveau module et réutilisée en RÉ-ENTRANCE (le parsing récursif sur le label d'un lien / l'intérieur d'un gras/italique appelaitrenderInline()récursivement avec le MÊME objetRegExp). Le dernierexec()d'un appel imbriqué qui retournenullréinitialise automatiquementlastIndexà 0 côté moteur JS — la bouclewhilede l'appel PARENT reprenait alors sa recherche depuis le début du texte au lieu de continuer où elle en était, rejouant indéfiniment le premier lien/gras rencontré. Reproduit isolément : sur une ligne aussi simple qu'un gras + un lien, l'ancienne logique ne termine jamais (confirmé — abandon manuel après 20 000 itérations en test isolé, sans le fix). - ·Tokenizing extrait en fonction pure —
src/lib/articleInline.ts(nouveau, aucune dépendance React/JSX) :tokenizeInline(text, depth)instancie une regex fraîche (new RegExp(...)) à CHAQUE appel, y compris chaque appel récursif — jamais d'objet partagé. La branche "run arabe" est strictement une feuille (jamais de récursion dessus, 2ᵉ cause potentielle identifiée). Récursion sur lien/gras/italique bornée àMAX_INLINE_DEPTH = 3(au-delà, contenu traité en feuille viatokenizeArabicOnly()). Garde-fou durMAX_INLINE_ITERATIONS = 5000dans la bouclewhile(throw silencieux → coupe et retourne, ne devrait jamais se déclencher avec les protections ci-dessus mais protège contre toute régression future). - ·
ArticleContent.tsx—renderInline()/INLINE_RE(module-level) remplacés par un appel àtokenizeInline()(logique pure) +renderTokens()(mapping JSX pur, aucune regex).parseBlocks(), la détection des paires AR/FR (système bilingue des articles) et le composantades blocsraw(liens/mot/...→next/link, ajouté en v1.22.10) sont inchangés — l'objectif de v1.22.10 reste acquis :**gras**rend du gras,[texte](/mot/x)rend un lien cliquable, dans les blocssection/bilingualcomme dans les blocsraw. - ·Nouveau test
src/lib/articleInline.test.ts(npm run test:article-inline) — 6 cas (ligne 100% arabe vocalisée, arabe+lien interne, terme arabe en gras, FR avec lien+gras mélangés, ligne mixte AR+FR, cas adversarial avec formations répétées denses) + vérification du garde-fou. Chaque cas doit se terminer sans exception en moins de 50ms. Lenpm run buildde ce projet ne peut pas détecter ce type de bug (pas de Supabase dans le sandbox → génération statique des pages articles jamais exercée, le rendu réel n'est jamais exercé) : ce test runtime est donc la seule garde-fou de non-régression pour ce composant. Vérifié : le test échoue (boucle non-terminante) contre l'ancienne implémentation, passe contre la nouvelle. - ·CLAUDE.md — 2 nouvelles règles permanentes (§ Gotchas développement) : regex
/gjamais partagée en ré-entrance récursive (toujoursnew RegExp(...)local à la fonction) ; tout parsing récursif basé sur une regex/bouclewhiledoit avoir un test runtime de terminaison dédié,tsc --noEmit/npm run buildne suffisent pas dans cet environnement.
v1.22.10 — Juillet 2026
- ·Fix
ArticleContent.tsx— syntaxe markdown affichée littéralement —**gras**et[lien](/mot/...)s'affichaient tels quels (astérisques/crochets visibles) dans les paires de lignes AR/FR (blocssection/bilingualdeparseBlocks(), ex: titres arabes + sous-titre français, lignes séparées par<br>). Cause confirmée en base (articlela-chaleur-en-arabe-classique) : ces blocs ne passent jamais parReactMarkdown— seuls les blocsrawen bénéficiaient — et étaient rendus via un simple<p>{texte}</p>/renderFrLine()(isolation bidi arabe uniquement, aucun parsing markdown). - ·
renderFrLine()remplacée parrenderInline()— même isolation bidi des séquences arabes (dir="rtl", mêmes plages Unicode qu'avant), plus parsing inline pour lien[texte](url), gras**texte**, italique*texte*(récursif sur le contenu imbriqué). Appliquée aux lignesarETfrdes blocssection/bilingual(le AR peut aussi contenir un lien, cf. l'article test). Aucun changement àparseBlocks()ni à la logique de détection des paires AR/FR (système bilingue des articles non touché). - ·Liens internes
/mot/...→<Link>Next.js —renderInline()route les hrefs commençant par/versnext/link, le reste vers<a target="_blank" rel="noopener noreferrer">. Même traitement ajouté aux blocsrawexistants via un nouveau composantadans lescomponentsdeReactMarkdown(AnchorComponent, absent auparavant — seulstrongétait overridé pour le raccourci "mot en gras = lien vers sa fiche"). - ·Vérifié : regex testée directement contre la ligne réelle en base (
نَقُولُ [صَحْرَاءُ](/mot/sa7raa2-tn) عَنْ...) — lien et gras correctement extraits, séquences arabes isolées individuellement.
v1.22.9 — Juillet 2026
- ·Fix autocomplete — mot anglais tapé directement introuvable — le mode
field='fr'deautocompleteSearch()(api/search/route.ts) ne cherchait (via la RPCsearch_df_trad) quetranslation_fr: taper "know"/"help"/"dishes" directement ne remontait rien (il fallait taper la traduction française pour voir apparaître le mot). Ajout d'un ILIKE séparé surtranslation_en(jamais.or()avec variable — règle PostgREST virgule), en parallèle de la RPC, fusionné paridvia leSetde dédup existant. La RPCsearch_df_tradelle-même n'est pas touchée (v1.22.8 documentait déjà pourquoi :RETURNS TABLE(...)explicite, risque d'overloads dupliqués si modifiée — cf. v1.14.2/v1.14.3) ; l'enrichissementtranslation_enpour l'affichage des lignes RPC (déjà en place depuis v1.22.8) est conservé tel quel. - ·
scoreRow()étendu àtranslation_en— paramètre optionneltranslationEn(7ᵉ arg, rétro-compatible) propagé àscoreWordRelevance()(déjà capable de le prendre en compte depuis v1.22.7) ;matchType='trad'déclenché aussi sur exact/préfixe/contenu detranslation_en, même priorité quetranslation_fr. Appliqué à tous les appels descoreRow()(secondarySearch,dfRowToSuggestion, RPC MSA) pour cohérence, sans changer le ranking existant (arabe/latin > traduction > exemple). - ·Pas de priorisation par locale côté serveur — les deux champs sont cherchés sans distinction (le fallback explicitement accepté par la demande : détecter la locale serveur aurait nécessité un paramètre supplémentaire côté client non nécessaire ici). Tabs Transliterated/Arabic/French-English inchangés.
v1.22.8 — Juillet 2026
- ·Fix autocomplete — traduction affichée figée en français — les suggestions de
SearchBar.tsxaffichaient toujourstranslation_fr, même en locale EN.Suggestion(api/search/route.ts) gagne un champtranslation_en: ajouté auxselect(...)explicites (secondarySearch, modearab, variantes phonétiques, pool fallback 200-lignes) et lu directement sur les lignes RPCsearch_df_fuzzy/search_words_fuzzy(RETURNS SETOF dialect_forms/words— colonne déjà présente, aucun changement RPC). Seulesearch_df_trad(modefr,RETURNS TABLE(...)explicite sanstranslation_en) ne l'expose pas : enrichissement par un lookupidséparé après l'appel RPC plutôt que modifier sa signature — évite le bug des overloads dupliqués surRETURNS TABLEdéjà documenté (v1.14.2/v1.14.3). Aucune RPC ni logique de matching touchée. - ·
SearchBar.tsx: helpertranslationFor(s)— retournetranslation_ensilocale==='en'et non vide, sinon fallbacktranslation_fr. Utilisé dans les 3 modes d'affichage (arabe/français/latin) y compris pour le texte passé à<HighlightText>— le surlignage matche donc désormais le bon champ selon la locale (ex: taper "know" surligne danstranslation_en, pastranslation_fr). - ·Tab "Français"/"English" adaptatif — le pill de mode
fr(recherche par traduction) affiche désormaist('search.fieldEn')("English"/"Anglais") en locale EN au lieu de traduire littéralement le mot "French". Nouvelle clésearch.fieldEndansfr.json/en.json. Aucune auto-détection de langue de la query n'existait déjà côté mode — non ajoutée (hors-scope, RPC backend inchangée).
v1.22.7 — Juillet 2026
- ·Fix recherche par
translation_en— le modefield='fr'desearchWords()(db.ts, mode par défaut de/dictionnaire) ne cherchait quetranslation_fr+example_fr: un mot tapé en anglais ne remontait jamais. Ajout d'un 3ᵉ ILIKE parallèle surtranslation_en(jamais.or()— même règle PostgREST que les autres colonnes), danssearchDf()(dialect_forms) etsearchMsa()(wordsMSA), fusionné dans l'ordretranslation_fr → translation_en → example_fr.translation_enétait déjà retourné par lesselect('*')existants, aucun changement de SELECT nécessaire. - ·Tri de pertinence étendu à
translation_en—scoreWordRelevance()(search-utils.ts) accepte un 7ᵉ paramètre optionneltranslationEn(rétro-compatible, tous les appels existants inchangés) : exact/préfixe/contenu surtranslation_enévalués au même niveau de priorité quetranslation_fr(0.77/0.75/0.72). Ranking existant (arabe/latin > traduction > exemple) inchangé. Appel de tri danssearchWords()mis à jour pour passertranslation_en. - ·Placeholder barre de recherche — mention des 3 langues —
search.placeholderFr(fr.json/en.json), affiché par défaut sur/dictionnaire(modefrinitial), disait uniquement "Chercher une traduction française…" / "Search a French translation…". Corrigé en "Chercher en arabe, français ou anglais…" / "Search in Arabic, French or English…" — déjà conditionné paruseLocale()/t(), aucun changement de code dansSearchBar.tsx.
v1.22.6 — Juillet 2026
- ·Fiche article — bloc de partage remonté sous le header — le bloc "Partager cet article" (
src/app/articles/[slug]/page.tsx) était placé après le contenu Markdown et la navigation série, tout en bas de page. Déplacé juste après le bloc Auteur/date, avantArticleContent— visible sans scroller jusqu'au bas de l'article. Déplacement JSX pur, logique de partage (liens Twitter/WhatsApp) inchangée. - ·Icône WhatsApp officielle —
public/icons/whatsapp.png(128×128, généré à partir du path officielsimple-icons— fond vert#25D366, glyphe blanc — viasharp, aucun asset copié depuis un CDN externe non accessible depuis ce réseau). Remplace le texte seul "WhatsApp" par<img src="/icons/whatsapp.png" width={24} height={24} />+ libellé, cohérent avec le bouton Twitter à côté.
v1.22.5 — Juillet 2026
- ·Fiche mot — "Je l'utilise" remonté au-dessus du pli mobile — la section
WordContribution(bouton "Je l'utilise" + 3 autres tabs) est déplacée juste après le bloc hero (forme arabe + translittération + traduction), avant "Exemple d'utilisation"/"Équivalents dans d'autres dialectes" (grille Details), Morphologie, Famille de mots, Même schème, Déclinaisons, Régions d'usage et Action links. Objectif : sur mobile, "Je l'utilise" est visible sans scroller jusqu'en bas de la fiche. Aucun changement de logique de soumission — déplacement JSX pur danssrc/app/mot/[slug]/page.tsx.
v1.22.4 — Juillet 2026
- ·
WordCard— exemple arabe affiché en permanence — le propshowExample(n'affichait l'exemple que si!!query, donc seulement en résultats de recherche) est supprimé ; l'exemple arabe (word.example_phrase) s'affiche désormais dans toutes les situations oùWordCardest utilisé dès qu'il existe :/dictionnaire(browse ET recherche), page d'accueil ("Mots récents"),/racine/[racine], section mots liés de/mot/[slug]. Conditionword.example_phrase &&déjà en place — aucun espace vide laissé pour les mots sans exemple.
v1.22.3 — Juin 2026
- ·Fix recherche par mot-clé d'exemple — branches
field='fr'/'arab'/'latin'danssearchDf()etsearchMsa()— La recherche /dictionnaire retournaitfield='fr'(défaut) qui tombait sur un early-return limité àtranslation_fr, sans jamais toucherexample_fr. Même problème pourfield='arab'(ignoraitexample) etfield='latin'(ignoraitexample). Chaque branche réécrite avec des ILIKE parallèles sur les colonnes exemple (jamais.or()avec variable — règle PostgREST), dédup parid. Exemple concret : requête"montre"remonte désormaismounassib-tn(dontexample_fr= "Cette montre est appropriée pour l'occasion") alors qu'il était invisible auparavant.
v1.22.2 — Juin 2026
- ·Cohérence de la recherche autocomplete ↔ /dictionnaire — Les deux moteurs cherchent désormais dans les mêmes champs :
form_ar,form_latin,translation_fr,example,example_fr. Avant ce fix,/dictionnaireignorait les colonnesexample/example_fralors que l'autocomplete les couvrait viasecondarySearch(). Tri par pertinence ajouté danssearchWords(): exact (1.0) > prefix (0.9) > contenu ar/latin (0.8) > traduction (0.72–0.77) > exemple (0.60). Fonction partagéescoreWordRelevance()extraite danssearch-utils.ts, utilisée pardb.tset/api/search/route.ts.WordCard: nouveau propshowExample— affiche l'exemple arabe sous le mot dans les résultats de recherche (donnée de contexte visuel, surtout utile quand le match vient de l'exemple).
v1.22.1 — Juin 2026
- ·Version anglaise — Phase 2 : traductions DB affichées — Les champs
translation_enetexample_en(colonnes Supabase, ~2191 mots enrichis) sont désormais utilisés à l'affichage quandlocale === 'en'. Composants corrigés :WordCard(grille dictionnaire), fiche/mot/[slug](bloc Traduction + traduction de l'exemple, via<T>),WordFamily(famille de mots / même racine),PatternMatches(mots de même schème).SynonymListinchangé (affiche arabe + translittération uniquement). DB :example_translation_enajouté au typeWord(mappé depuisexample_enen base),translation_enajouté aux selects et mappers degetWordsByPattern/getWordsByRacinevia les interfacesPatternMatch/FamilyMatch.
v1.22.0 — Juin 2026
- ·Version anglaise — Phase 1 complète — Toggle FR/EN dans la Navbar (cookie
NEXT_LOCALE), aucun changement d'URL ni d'ISR.LocaleProvider+useLocale()+t()danssrc/lib/i18n.tsx. Composant<T fr="..." en="..." />pour les pages server ISR. 215 clés de traduction danssrc/locales/fr.json+src/locales/en.json. Toutes les pages publiques couvertes : homepage, dictionnaire, mot/[slug], racines, racine/[racine], qcm, qcm/[slug], articles, articles/[slug], articles/proposer, ajouter, guide-phonétique, a-propos. Tous les composants UI traduits : Navbar, SearchBar, RacineSearchBar, DialectFilter, POSFilter, WordCard, WordFamily, WordMorphology, SuggestEdit, WordContribution, QuizPlayer, ShareButton. SEO (generateMetadata) et ISR inchangés côté serveur.
v1.21.6 — Juin 2026
- ·Fix noms variables Upstash —
loginRateLimit.tscorrigé pour lireKV_REST_API_URLetKV_REST_API_TOKEN(convention Vercel Storage × Upstash) au lieu deUPSTASH_REDIS_REST_URL/TOKEN(convention Upstash brute). Noms confirmés depuis le dashboard Vercel de l'intégration "protect-admin".
v1.21.5 — Juin 2026
- ·Rate limiting login — migration in-memory → Upstash Redis —
src/lib/loginRateLimit.tsmigré deMapen mémoire vers@upstash/redis(cléadmin:login:{ip_hash}, TTL 30 min, nettoyage automatique Redis). Protection désormais cross-instances Vercel (persistée en dehors des lambdas). Variables attendues :UPSTASH_REDIS_REST_URL+UPSTASH_REDIS_REST_TOKEN. Fallback gracieux si Redis indisponible : rate limiting désactivé mais le mot de passe reste vérifié côté serveur.@upstash/ratelimitaussi installé mais non utilisé ici (compte toutes les requêtes, pas seulement les échecs — incompatible avec la sémantique "5 failures" demandée). Comportement utilisateur inchangé : mêmes messages, mêmes seuils.
v1.21.4 — Juin 2026
- ·Sécurité — brute-force protection sur
/admin— rate limiting sur/api/admin/login: 5 tentatives échouées en 15 min → blocage 15 min (429). Implémenté viasrc/lib/loginRateLimit.ts(Map en mémoire dédiée, séparée du rate limiter contributions). Le compteur se réinitialise après un login réussi. Avertissement progressif dès les 2 dernières tentatives restantes ("X tentative(s) restante(s) avant blocage"). Délai 500ms conservé sur chaque échec (ralentit en dessous du seuil). Limitation connue : en mémoire, non persisté entre instances Vercel — suffisant pour un site mono-admin ; upgrade Supabase/Upstash si besoin de persistance inter-instances. - ·Sécurité — noindex sur
/admin—src/app/admin/layout.tsxcréé (server component) avecrobots: { index: false, follow: false }→ balise<meta name="robots" content="noindex,nofollow">générée par Next.js.Disallow: /adminajouté àpublic/robots.txt. Double protection : robots.txt indique aux crawlers de ne pas visiter,noindexleur interdit d'indexer même si visitée directement.
v1.21.3 — Juin 2026
- ·Fix DYNAMIC_SERVER_USAGE sur
/racine/[racine]—export const dynamic = 'force-dynamic'restauré (retiré par erreur en v1.14.3 puis re-confirmé nécessaire). Cause précise :(await searchParams).dialectest lu engenerateMetadataet dans le composant page — accéder àsearchParamsdans une page avecrevalidate(ISR) est incompatible avec le rendu statique, Next.js lançait une erreurDYNAMIC_SERVER_USAGE(1989 occurrences dans les logs Vercel depuis le 23 juin).revalidate = 60retiré en cohérence avecforce-dynamic(ISR n'a plus de sens une fois le rendu contraint par-requête). Règle permanente déjà documentée dans CLAUDE.md v1.14.3 : tout segment dynamique +searchParamsdoit porterforce-dynamic. - ·Page d'erreur 500 thématisée —
src/app/error.tsxcréé (composant client, error boundary App Router). Affiche « خطأ » en grand (même style que la page 404), titre "Une erreur est survenue", bouton "Réessayer" (reset()) + lien retour accueil. Corrige le second symptôme des logs : "Failed to load static file for page: /500 ENOENT" (663 occurrences) — Next.js ne pouvait pas servir de page 500 car aucun error boundary n'était défini.
v1.21.2 — Juin 2026
- ·HeroSlideshow desktop — 5 images paysage —
SLIDES_DESKTOPremplacé par les vrais cheminspublic/heroimagesweb/: Chefchaouen ruelle bleue (MA), Aït Benhaddou kasbah (MA), Sidi Bou Said coucher de soleil (TN), deux photos de dunes (coucher + lever de soleil). Responsive maintenu : mobile continue avecpublic/hero/(4 images portrait inchangées), desktop bascule sur les nouvelles images à partir demd:.
v1.21.1 — Juin 2026
- ·Fix font-bold sur texte arabe dans les listes — SynonymList, WordFamily, PatternMatches, DeclinaisonsSection :
font-bold→font-normal(400) sur les mots arabes affichés dans les items de liste — cohérent avec le fix appliqué sur WordCard en v1.20.0. La règle "Readex Pro poids 400 partout" est maintenant complète sur toutes les vues publiques de /mot/[slug]. - ·HeroSlideshow — structure responsive mobile/desktop — refactorisé en deux blocs Tailwind (
md:hidden/hidden md:block) avecSlideshowInnerpartagé. Desktop prêt à recevoir les imagespublic/heroimagesweb/dès que Mehdi les pousse sur main (SLIDES_DESKTOP = SLIDES_MOBILE en attendant — aucune régression). - ·Fix contraste titre hero sur images dorées —
text-shadowdouble couche ajouté sur h1 (0 2px 10px rgba(0,0,0,.55), 0 1px 3px rgba(0,0,0,.35)) et surعاميةmobile (0 2px 12px rgba(0,0,0,.65), 0 1px 4px rgba(0,0,0,.45)). La shadow est héritée par le<em>doré "des dialectes arabes" — titre lisible sur n'importe quelle image, y compris fond jaune/orange (dunes coucher de soleil).
v1.21.0 — Juin 2026
- ·Refonte DialectBadge — drapeau plein, sans fond coloré — les badges dialecte (WordCard, SynonymList, WordFamily, PatternMatches, fiche mot
/mot/[slug]) affichent désormais le drapeau SVG remplissant un carré à coins arrondis (2px), sans fond coloré derrière. Implémentation : propsquareajoutée àFlagSVG(preserveAspectRatio="xMidYMid slice"), composantDialectBadgeentièrement réécrit (carré 22/28/32px selonsize). Le code dialecte (TN, MA…) ou le nom complet (showName) s'affiche à côté du carré, jamais par-dessus. Suppression de la class CSS.dialect-badge(plus utilisée).
v1.20.1 — Juin 2026
- ·Fix hero /a-propos — fallback image si vidéo bloquée — section hero de
/a-propos: ajout d'unbackground-imageCSS (tataouine-troglodyte.jpg) directement sur la<section>. Si le navigateur bloque l'autoplay de la vidéo (mobile, politique stricte), l'image reste visible en fond sous l'overlay. HeroVideo joue par-dessus quand il peut. - ·Fix tirets cadratins /a-propos — 4 em-dashes dans le bloc Présentation (étymologie zeedna) remplacés par une construction à deux-points et parenthèses ; 1 em-dash section "Pourquoi" → deux-points ; 1 em-dash section "Rôle IA" → deux phrases distinctes avec virgule. Ton plus naturel, moins "généré".
v1.20.0 — Juin 2026
- ·Refonte typographie arabe — Amiri → Readex Pro partout — décision de simplification : Amiri jugée trop lourde en usage courant. Nouvelle règle unique : Readex Pro (
font-ar-display+font-arabic+.section-ar→ tous redirigés versvar(--font-readex)) pour tout texte arabe sur le site. Seule exception conservée : le logo "زيدنا عامية" dans la Navbar (classe.zeedna-brand-name, Amiri inchangée). Ajustement poids sur WordCard (mot arabe 32px) :font-bold→font-normal. Polices des champs d'input arabes (SuggestEdit, FieldProposal) :var(--font-arabic)→var(--font-readex)
v1.19.1 — Juin 2026
- ·Fix Cormorant Garamond poids non déclaré —
font-translitne fixait quefont-familyvia Tailwind, laissantfont-weight: 400hérité du body. Le navigateur devait résoudre le @font-face 300 italic par matching implicite (parfois instable selon le moteur). Correction : règle CSS.font-translit { font-weight: 300; font-style: italic }ajoutée dansglobals.csshors de tout@layer(priorité cascade > utilities Tailwind) → match exact et déterministe sur le @font-face chargé parnext/font/google
v1.19.0 — Juin 2026
- ·Chantier typographie — translittération — Cormorant Garamond 300 italic (
--font-cormorant,font-translitTailwind) remplacefont-mono/font-sanspour toutes les translittérations : fiche mot hero, WordCard, SynonymList, WordFamily, PatternMatches, DeclinaisonsSection, WordMorphology (tables conjugaisons + formes dérivées), SearchBar (modes arab, fr, latin), hero landing (cartes featured + mot du jour) - ·Chantier typographie — polices arabes — WordCard : mot arabe 32px passe de Readex Pro à Amiri (
arabic-text font-ar-display) — uniformisation avec la règle display vs UI déjà appliquée aux autres composants - ·Fix tri "Mots récents" — sort
parseInt(b.id)→new Date(b.created_at).getTime()(les IDs sont des chaînes TEXT, pas des entiers, ce qui donnait un ordre aléatoire)
v1.18.2 — Juin 2026
- ·Corrections hero landing — per-image
objectPositionsurHeroSlideshow(desert-1 :center 80%, desert-2 :center 65%, desert-3 :center 70%, tataouine :center 35%) +quality={90}pour limiter l'artefact JPEG sur les images claires ; section CTA Contribuer supprimée de la landing (redondante avec les boutons dans le Hero) - ·Contenu
/a-proposenrichi — 5 nouvelles sections : "Pourquoi ce dictionnaire" (problème des 400M locuteurs sans ressource structurée), "Ajouter un mot en 30 secondes" (guide en 3 étapes numérotées), "Ce que fait l'IA" (rôle de Gemini 2.5 Flash + validation humaine), "De la soumission à la publication" (4 étapes du pipeline), "Feuille de route" (QCM ✓, racine ✓, dialectes, audio, mobile)
v1.18.1 — Juin 2026
- ·Hero → diaporama d'images —
HeroVideoremplacé parHeroSlideshowsur la landing page : 4 images enpublic/hero/(dunes, coucher de soleil, campement berbère, troglodytes de Tataouine), crossfade CSSopacity 1.5sautomatique toutes les 7 secondes,next/imageavecfill+sizes="100vw"+prioritysur la première image, overlay gradient légèrement renforcé pour assurer le contraste sur les images claires (notamment le troglodyte blanc) - ·Réorganisation sections landing — nouvel ordre : Hero → Articles récents → Mot du jour + Grille des dialectes → Mots récents → CTA Contribuer
- ·Vidéo →
/a-propos—HeroVideo(3 vidéos en fond) déplacée en arrière-plan de la section introductive de/a-proposavec overlay sombrergba(10,7,3,.65/.55)pour la lisibilité du texte
v1.18.0 — Juin 2026
- ·Refonte landing page — réalignement complet sur le mockup de référence : nouvel ordre des sections (Hero → Mot du jour + Grille des dialectes → Mots récents → Articles récents → CTA Contribuer) ; ancienne section "Dialectes du monde arabe" supprimée
- ·Mot du jour — nouvelle carte fond
#161008avec pattern SVG losanges (opacité 3%), sélection pseudo-aléatoire stable par jour (eligible.sort(id).at(dayIndex % n)), badge dialecte pill, mot arabe Amiriclamp(52px, 7vw, 96px), séparateur ZeednaDivider, traduction DM Serif Display 38px#E8C460, encadré exemple avec bordure gauche dorée (arabe + traduction FR italique), bouton "Voir la fiche complète" + lien partage Twitter (server-safe, pas dewindow.location) - ·Grille des dialectes — 3×2 : top 5 dialectes par nombre de mots + 1 carte pointillée "+N autres" ; chaque carte avec emoji drapeau, nom FR, nom arabe, lettre watermark, compteur de mots
- ·
ArticleCardvariantphoto— image fixe 160px, gradient sombre overlay, badges catégorie + dialectes en overlay bas-gauche, titre arabe conditionnel, titre FR 17px DM Serif Display, excerpt, date + "Lire →" ; landing utilise désormaisvariant="photo"sur les 3 articles récents - ·
getRecentPublishedArticles— champtitre_arajouté au SELECT (était absent, nécessaire pour le variant photo)
v1.17.2 — Juin 2026
- ·Fix 500 OG image (root cause) —
/mot/[slug]/opengraph-image:await r.arrayBuffer()manquant dansloadFont()(rejet non capturé par le try/catch) ;new ImageResponse(...)enveloppé dans try/catch avec fallback ; footer remplace le texte arabeexample(crash Satori :fontStyle:'italic'sans police italique chargée) parexample_translation_fr(français, pas de style italique) ; null guards?? ''sur tous les champsword
v1.17.1 — Juin 2026
- ·Fix egress Supabase (Fair Use Policy) —
/articles/[slug]et/mot/[slug]repassent en ISR (revalidate=3600) au lieu deforce-dynamic;/racine/[racine]: suppression deforce-dynamic(revalidate=60 inchangé) ; les 3 routes bénéficient de revalidatePath côté admin pour propagation immédiate après édition - ·Fix 500 sur
/mot/[slug]/opengraph-image—getWordBySlugenveloppé dans.catch(() => null); aussi parallélisation font+DB fetch viaPromise.all - ·Cache-Control sur API publiques —
/api/racines/count(s-maxage=60) et/api/words/[slug](s-maxage=300) : les réponses 200 sont désormais cachées par le CDN Vercel - ·Nettoyage — logs de diagnostic
console.logretirés de/racine/[racine]et/articles/[slug]
v1.17.0 — Juin 2026
- ·Titre arabe sur les articles — colonne
titre_ar(nullable) surfacée sur 3 points : page détail article (titre Amiri doré#E8C460au-dessus du titre français DM Serif Display blanc cassé), cartesArticleCard(ligne arabe#C9A84Cen petit au-dessus du titre FR, sur landing et/articles), formulaire admin (champdir="rtl"optionnel) ; APIs PATCH + POST mises à jour
v1.16.5 — Juin 2026
- ·Refonte
/a-propos— design system Qirtâs × Amiri — cornersrounded-2xl→--z-radius-sm(2px) sur les cartes statistiques et la carte de présentation ; fonds →--z-bg-surface; valeurs statistiques et h1 →font-fr-display(DM Serif Display) ; texte de présentation →font-fr-ui(DM Sans)
v1.16.0 — Juin 2026
- ·Upload d'image de couverture par article — nouvelle route
POST /api/admin/articles/upload-image: upload vers le bucket Supabase Storage "article-images" (service_role), chemin{slug}/{timestamp}-{filename}, validation type (jpg/png/webp) + taille (5 Mo), suppression de l'ancienne image si remplacement - ·Formulaire admin enrichi — le champ "Image URL" de l'onglet 📝 Articles se transforme en vrai upload avec bouton "Choisir une image", preview live de l'image, fallback texte pour URL manuelle, indicateur de chargement, gestion des erreurs
- ·Section "Derniers articles" sur la landing page — 3 articles les plus récents (status='published') affichés en grille après "Derniers mots ajoutés" ; la section est masquée si aucun article n'est publié
- ·
ArticleCardextrait en composant partagésrc/components/ArticleCard.tsx(utilisé par/articles, la landing, et extensible) ;formatArticleDate()exportée - ·
getRecentPublishedArticles(n)ajouté àsrc/lib/db.ts
v1.15.1 — Juin 2026 (hotfix)
- ·Fix logo — nouveau SVG simplifié (5 éléments, viewBox 44×72,
currentColor) remplaçant l'ancien encensoir complexe dansZeednaLogo.tsxetpublic/logo.svg(favicon) - ·Fix polices — variables CSS des fontes (
--font-amiri,--font-readex,--font-dm-serif,--font-dm-sans) déplacées de<body>vers<html>danslayout.tsx— pattern officiel Next.js/next-font, garantit que les variables sont disponibles dès la racine de la cascade CSS
v1.15.0 — Juin 2026 (redesign)
- ·Redesign Qirtâs × Amiri — nouvelle direction visuelle sur toutes les pages principales
- ·Typographie : Amiri (arabe, titres décoratifs), Readex Pro (arabe UI), DM Serif Display (titres français), DM Sans (corps français)
- ·Design tokens CSS
--z-*: palette dorée/terre/sable, dark-first (:rootsombre,.lightoverride), bordures carrées (--z-radius-sm: 2px,--z-radius-md: 3px), pattern géométrique SVG sur le fond - ·Composants :
FlagSVG(22 drapeaux SVG inline + icône MSA),DialectBadge,ZeednaDivider,ScoreRing - ·Pages redessinées : homepage (
/), dictionnaire, fiche mot, articles, QCM listing et détail - ·Composants partagés : WordCard, SynonymList, WordFamily, PatternMatches, Navbar, DialectFilter
- ·Branch :
redesign-qirtas-amiri(à merger sur main)
v1.14.4 — Juin 2026 (fix)
- ·Fix 404 persistant sur
/articles/[slug]pour les articles nouvellement publiés — cause : le cache ISR (revalidate = 3600) pouvait mémoriser une réponse 404 si quelqu'un visitait l'URL avant publication (article encore endraft). Après publication via/admin, le cache n'était pas invalidé → la 404 restait servie pendant 1 heure - ·Fix : ajout de
revalidatePath('/articles/{slug}', 'page')+revalidatePath('/articles', 'page')dansPATCH /api/admin/articles/[id](édition/publication) etPOST /api/admin/articles(création directement publiée). Dès qu'un article est publié ou modifié via l'admin, le cache ISR de sa page est immédiatement invalidé — prochaine visite re-génère la page depuis la DB - ·La structure de
/articles/[slug]/page.tsxétait déjà correcte (dynamicParams = true,generateStaticParams = []) — la solution ciblait uniquement l'invalidation de cache au bon moment - ·Audit des autres routes dynamiques :
/mot/[slug](dynamicParams = true,revalidate = 60,generateStaticParams = []) et/racine/[racine](dynamicParams = true,revalidate = 60,force-dynamic) sont correctes — pas de correctif nécessaire
v1.14.3 — Juin 2026 (hotfix)
- ·Fix réel du 500 persistant sur
/racine/[racine]— le fix DB de v1.14.2 (overloads RPC dupliqués) était nécessaire mais pas suffisant : le 500 a continué en production après coup (x-matched-path: /500, confirmé par requête HTTP directe surzeednalearn.com), preuve que le crash venait du code Next.js, pas de la base (la RPCget_words_by_racine_normalizedretourne des résultats corrects en appel direct) - ·Cause :
/racine/[racine]/page.tsxest la seule route du projet combinant un segment dynamique ([racine]) avecsearchParamslus en page + engenerateMetadata, sous ISR (revalidate = 60,dynamicParams = true), sansexport const dynamic = 'force-dynamic'./articles/page.tsx(searchParams seul, pas de segment dynamique) a déjà ce flag avec le commentaire// searchParams = dynamic— seule la combinaison segment dynamique + searchParams n'était pas couverte./mot/[slug](segment dynamique, pas de searchParams) et/racines(searchParams, pas de segment dynamique) fonctionnent tous les deux normalement en prod, validant que c'est bien cette combinaison précise qui posait problème - ·Fix : ajout de
export const dynamic = 'force-dynamic'sur/racine/[racine]/page.tsx - ·Instrumentation ajoutée (diagnostic, conservée) :
console.log('[racine] params reçus:', racine, dialect)avant l'appel RPC (page +generateMetadata),getWordsByRacineGroup()désormais entourée d'untry/catchavecconsole.error('[racine] erreur RPC:', error)— l'appel n'était auparavant protégé par aucuntry/catch, laissant toute erreur remonter telle quelle au handler 500 générique de Next.js sans log applicatif
v1.14.2 — Juin 2026 (hotfix)
- ·Fix critique 500 Internal Server Error sur
/racine/[racine]— les migrationsadd_dialect_filter_to_get_words_by_racine_normalized(v1.14.1) etadd_dialect_filter_to_search_racine_groups(v1.14.0), qui ajoutaient un paramètrep_dialectviaCREATE OR REPLACE FUNCTION, ont chacune créé un overload Postgres dupliqué au lieu de remplacer la fonction existante (signature de paramètres différente ⇒ Postgres traite ça comme une nouvelle fonction distincte plutôt qu'un remplacement). PostgREST se retrouvait avec 2 fonctions du même nom et ne pouvait plus choisir laquelle appeler ⇒ erreur d'ambiguïté côté Supabase, remontée en 500 générique côté Next.js - ·Diagnostiqué par introspection directe (
pg_proc/pg_get_function_identity_arguments) — confirmé 2 overloads pourget_words_by_racine_normalized(p_racineseul vsp_racine, p_dialect) et poursearch_racine_groups/search_racine_groups_latin(p_q, p_limitvsp_q, p_limit, p_dialect) - ·Fix :
DROP FUNCTION IF EXISTSciblé sur l'ancienne signature pour les 3 fonctions (migrationsdrop_old_single_arg_get_words_by_racine_normalizedetdrop_old_two_arg_search_racine_groups_overloads), ne laissant qu'un seul overload (le bon) par fonction. Vérifié par ré-introspection + appel RPC direct retournant des résultats corrects - ·Aucun changement de code applicatif — fix base de données uniquement, actif immédiatement sans redéploiement
- ·Règle à retenir pour toute future migration RPC additive : après un
CREATE OR REPLACE FUNCTIONqui change la liste de paramètres d'une fonction existante, toujours vérifier viapg_procqu'il ne reste qu'un seul overload —tsc/npm run buildne détectent pas ce genre de régression côté base de données
v1.14.1 — Juin 2026
- ·Masquage du "#" à l'affichage des racines — le placeholder CamelTools (lettre faible non résolue) ne doit jamais être visible côté utilisateur, seulement structurel en base. Nouveau
src/lib/racine.ts:sanitizeRacineDisplay()(remplace#par le tatwil arabeـ) etformatRacineWithSeparators()(ex-formatRacine(), dupliquée dansWordMorphology.tsx/WordFamily.tsx, désormais centralisée). Appliqué partout où une racine est affichée :/racine/[racine],/racines,RacineSearchBar, section "Morphologie" et "Famille de mots" sur/mot/[slug]. Les valeurs utilisées pour la navigation/les requêtes DB (href,encodeURIComponent, RPC) restent la racine brute, jamais sanitizée — seul le rendu visuel change - ·Propagation du filtre dialecte sur
/racine/[racine]— la page de détail d'une racine ignorait jusqu'ici le dialecte sélectionné en amont (mots de tous les dialectes mélangés même après un filtre TN sur/dictionnaire). RPCget_words_by_racine_normalized(p_racine, p_dialect DEFAULT NULL)(migrationadd_dialect_filter_to_get_words_by_racine_normalized) — nouveau paramètre optionnel additif, rétro-compatible.src/lib/db.ts:getWordsByRacineGroup(racine, dialect?)transmetp_dialect - ·
?dialect=XXpropagé dans les liens vers/racine/[racine]depuisRacineSearchBar(suggestions) et/racines(cartes), comme déjà fait pour/racineselle-même - ·
/racine/[racine]affiche un badge "Filtré : {dialecte}" (même style que celui de/racines) avec bouton de retrait quand un filtre est actif ;generateMetadatareflète aussi le filtre dans le compte de mots
v1.14.0 — Juin 2026
- ·Filtre dialecte sur l'onglet "Par racine" de
/dictionnaire— auparavant seul l'onglet "Par mot" avait un filtre par dialecte ; "Par racine" cherchait toujours toutes les racines tous dialectes confondus - ·RPC
search_racine_groups(p_q, p_limit, p_dialect DEFAULT NULL)etsearch_racine_groups_latin(p_q, p_limit, p_dialect DEFAULT NULL)(migrationadd_dialect_filter_to_search_racine_groups) — nouveau paramètre optionnel additif,p_dialect IS NULLpréserve le comportement existant pour tout appelant non modifié.GET /api/search-racinelit?dialect=et le transmet.get_words_by_racine_normalized(page/racine/[racine]) volontairement inchangée — hors scope, cette page affiche déjà les mots groupés par dialecte - ·
RacineSearchBar.tsx: nouvelle propdialect?: string, incluse dans l'URL de fetch et les deps du debounce - ·
dictionnaire/page.tsx(mode "Par racine") : bloc "Filtrer par dialecte" (DialectFilter, même composant que le mode "Par mot") au-dessus deRacineSearchBar— pas de filtre type de mot (pos_typen'a pas de sens au niveau racine, plusieurs mots d'une même racine peuvent avoir despos_typedifférents). Sélectionner "MSA" ne retourne que les racines ayant au moins un mot classique, utile pour isoler les racines classiques pures - ·
src/lib/db.ts:getAllRacinesGrouped(dialect?: string)— filtre.eq('dialect', ...)conditionnel, alimente à la fois le compteur et/racines - ·
GET /api/racines/countaccepte désormais?dialect=. Le compteur "Parcourir toutes les racines (N)" se met à jour à chaque changement de dialecte en mode "Par racine" - ·
/racines?dialect=XX— le filtre se propage depuis le lien "Parcourir toutes les racines" : la page liste alors uniquement les racines du dialecte sélectionné (titre/description SEO adaptés, badge de filtre actif avec bouton de retrait)
v1.13.0 — Juin 2026
- ·Parcours alphabétique de toutes les racines — nouvelle page
/racineslistant les 922 racines distinctes du dictionnaire, groupées par lettre initiale selon l'alphabet arabe traditionnel (alefbaa hijaiyya), pas l'ordre Unicode brut. Complément à la recherche libre par racine existante (RacineSearchBar//api/search-racine//racine/[racine]), aucun des trois n'est modifié - ·
src/lib/arabicAlphabet.ts(nouveau) —ARABIC_ALPHABET_ORDER,normalizeArabicLetterForSort()(variantes alef/hamza regroupées pour le tri),getArabicLetterRank(),sortByArabicAlphabet() - ·
src/lib/db.ts:getAllRacinesGrouped()— agrégation côté JS (pas de nouvelle RPC/migration, volume vérifié avant implémentation : 1545 lignes, largement suffisant pour unGROUP BYapplicatif) - ·
/racines(ISR 1h,generateMetadata) — nav rapide par lettre, sections groupées, cartes racine cliquables, section "Non classées" pour les racines#non résolues - ·
GET /api/racines/count(nouveau) — sert le badge dynamique "Parcourir toutes les racines (N)" ajouté sousRacineSearchBarsur/dictionnaire(mode "Par racine")
v1.12.0 — Juin 2026
- ·Module QCM (configuration + structure) — nouvelle section quiz du site, branchée sur les tables Supabase déjà créées (
quizzes,quiz_questions,quiz_answers, RLS déjà activé côté Supabase). Aucun contenu de quiz réel créé : tant qu'aucun quizstatus='published'n'existe en base,/qcmaffiche un état vide "QCM à venir 🕊️" avec liens retour dictionnaire/articles - ·Nouveaux types
Quiz/QuizQuestion/QuizAnswer+ unionsQuizLanguage/QuizCategory/QuizDifficulty/QuizStatus/QuizSource/QuizQuestionType(src/types/index.ts), etrelated_quiz_category/related_quiz_dialectajoutés àArticle - ·
src/lib/db.ts:getPublishedQuizzes(filtres),getQuizBySlug(slug)(quiz + questions + réponses imbriquées triées parorder_index),getQuizForArticle(category, dialect?),countPublishedQuizzes() - ·
GET /api/qcm(filtrescategory/dialect/difficulty/language) — utilisé par la page listing et par le badge "Bientôt" de la Navbar - ·
/qcm(listing, filtres style/dictionnaire, grille de cartes ou état vide) +/qcm/[slug](page quiz, ISR 60s,generateMetadatadynamique) + composant clientQuizPlayer(une question à la fois, single/multiple, feedback immédiat + explication, score final, support RTL silanguage='ar') - ·
Navbar.tsx: lien "QCM" entre Articles et Guide phonétique, badge doré "Bientôt" tant quecountPublishedQuizzes() === 0 - ·
/admin: nouvel onglet "🧩 QCM" (QuizzesTab) — liste tous statuts, création/édition de la structure (titre/slug auto/description/langue/catégorie/dialecte si catégorie dialectale/difficulté/statut), toggle publier/dépublier, suppression. Pas de formulaire questions/réponses pour l'instant. Nouvelles routesGET/POST /api/admin/quizzes+GET/PATCH/DELETE /api/admin/quizzes/[id](mêmes patternscheckAdminAuth/supabaseAdmin/logActionque/api/admin/articles) - ·
/articles/[slug]: sirelated_quiz_categoryest rempli, recherche un quiz publié correspondant (getQuizForArticle) et affiche un encart doré "Teste tes connaissances sur ce thème →" vers/qcm/{slug}; rien si aucun quiz trouvé
v1.11.0 — Juin 2026
- ·Recherche par racine en écriture arabe ET en translitération — extension de la recherche par racine (v1.10.0) : l'utilisateur peut désormais taper
د.ع.مoud3mindifféremment dansRacineSearchBar - ·Nouvelle colonne
dialect_forms.racine_latin(TEXT, migrationadd_racine_latin_column_and_search_rpc) — translittération Maghreb chiffrée deracine(points retirés), même convention queform_latin(3/7/9/5/gh/dh/T…) - ·
scripts/maghreb_translit.py: nouvelle fonctionracine_to_latin()— port direct caractère-par-caractère detranslateToLatin()(src/lib/transliteration.ts), sans logique chadda/diacritiques (une racine n'en contient jamais) - ·
scripts/backfill_racine_latin.py(nouveau) — backfill ponctuel one-shot : échantillon de 20 lignes toujours affiché avant écriture (dry-run par défaut,--apply+ confirmation expliciteOUI), lots de 50. Exécuté en prod : 1540/1544 mots avec racine ont désormaisracine_latin(4 racines non convertibles laisséesNULL, mêmes garde-fous que le reste du pipeline racine) - ·3 points d'écriture de
racinemis à jour pour maintenirracine_latinen cohérence :publishContribution.ts(calculeracine_latinviatranslateToLatin()à la publication),scripts/extract_racine_batch.pyetscripts/reconcile_racine_hash.py(calculentracine_latinviaracine_to_latin()à chaque écriture deracine) - ·Nouvelle RPC SQL
search_racine_groups_latin(p_q, p_limit)(migrationadd_racine_latin_column_and_search_rpc) — même formeTABLE(racine, word_count)quesearch_racine_groups(), maisILIKEsurracine_latin;GET /api/search-racinedispatch entre les deux RPC viaisArabicText(q)(déjà utilisée par/ajouter) — la racine retournée reste toujours en arabe, même quand le match s'est fait via le latin - ·
RacineSearchBar.tsx:dirdu champ devient dynamique (rtl/ltrselonisArabicText(query)), placeholder mis à jour pour mentionner les deux formats - ·Aucun changement sur
/racine/[racine](le param de route reste la racine arabe encodée, déjà géré par ledecodeURIComponentdéfensif de v1.10.0) ni sur l'affichage (racine toujours montrée en arabe avec points)
v1.10.0 — Juin 2026
- ·Recherche par racine trilitère/quadrilitère — nouveau mode de recherche en complément de la recherche par mot, sans toucher à l'existant (
SearchBar.tsx,/api/search,fetchWordsdedictionnaire/page.tsxinchangés) - ·2 nouvelles fonctions SQL Postgres (migration
add_search_racine_rpc_functions) :search_racine_groups(p_q, p_limit)(suggestions,GROUP BY racine+COUNT) etget_words_by_racine_normalized(p_racine)(liste complète) — toutes deux comparentREPLACE(racine, '.', '')des deux côtés (réutilise l'indexidx_dialect_forms_racine_normalizeddéjà en place) ; nécessaire carsupabase-js/PostgREST ne sait filtrer ni grouper sur une expression calculée sans RPC - ·
src/lib/db.ts: nouvelle fonctiongetWordsByRacineGroup(racine)— distincte degetWordsByRacine()(qui reste inchangée, utilisée par la section "Famille de mots" de/mot/[slug]) - ·
GET /api/search-racine?q=<arabe partiel>→{ ok: true, data: [{ racine, count }] }, limite 10,qvide → réponse immédiate sans appel DB - ·Nouvelle page
/racine/[racine](ISR 60s, comme/mot/[slug]) : mots groupés par dialecte (ordreTN, MA, DZ, EG, LB, MSApuis alphabétique), cartesWordCardréutilisées telles quelles, titre racine en grand format gold (#C9A84C), état vide avec lien retour/dictionnaire. Décodage défensif (decodeURIComponentavec fallback try/catch) du param de route — bug trouvé et corrigé en test local : sans ce décodage explicite, le param de route arrivait parfois encore encodé (%D8%AF...) au lieu d'être auto-décodé par Next.js, donnant 0 résultat malgré des mots existants en base - ·Nouveau composant
src/components/RacineSearchBar.tsx, autonome et séparé deSearchBar.tsx(mêmes idiomes : debounce 380ms,onMouseDown preventDefaultanti-race condition blur/click) — navigue directement vers/racine/[racine]au clic sur une suggestion - ·
dictionnaire/page.tsx: toggle additif "Par mot" / "Par racine" au-dessus de la barre de recherche — en mode racine, afficheRacineSearchBarà la place deSearchBaret masque filtres + grille (recherche racine = navigation directe, pas de listing inline) - ·
mot/[slug]/page.tsx+WordFamily.tsx: lien "Voir tous les mots de cette racine →" vers/racine/[racine], affiché seulement si la racine compte plus de 10 mots (fetch aveclimit=11au lieu de 10 pour détecter le dépassement, signature degetWordsByRacine()inchangée — juste l'argument au call site)
v1.9.2 — Juin 2026
- ·Fix pré-remplissage
/ajouterselon la langue tapée : le paramètre?mot=(envoyé parSearchBar.tsxetdictionnaire/page.tsxquand une recherche ne donne aucun résultat) était toujours injecté dans le champ « Mot en arabe », même quand l'utilisateur avait cherché en français/translittération — le mode restait aussi bloqué sur l'onglet arabe, ettranslateToLatin()était appliqué à du texte non-arabe (résultat incohérent) - ·Nouvelle fonction
isArabicText(text)(src/lib/transliteration.ts) — détecte la présence d'au moins un caractère dans les plages Unicode "Arabic" (U+0600–U+06FF) et "Arabic Supplement" (U+0750–U+077F), via comparaison de codepoints plutôt qu'une regex à séquences d'échappement\u(contournement d'un souci d'encodage rencontré pendant l'implémentation — fonctionnellement identique) - ·
ajouter/page.tsx: l'état initial deentryModeetformdérive maintenant deisArabicText(motParam)— texte arabe → champ arabe + onglet arabe (comportement inchangé) ; texte latin/français → champ traduction française + onglet français (nouveau) ; pas demotParam→ comportement par défaut inchangé - ·
not-found.tsx(404 générique) ne transmettait déjà aucun paramètremot— rien à corriger côté 404
v1.9.1 — Juin 2026
- ·Scan hebdomadaire automatisé des doublons :
.github/workflows/weekly-duplicates-scan.ymllancefind_duplicates.pytous les dimanches 9h Paris l'été / 8h l'hiver (cron: '0 7 * * 0'fixe, GitHub Actions ne gère pas le DST nativement — décision assumée plutôt qu'un double-cron + garde-fou horaire) ou manuellement viaworkflow_dispatch - ·
scripts/find_duplicates.py: nouvelle option--summary-json— écrit un résumé machine-readable (high_count,review_count,high_groups[]) en plus du rapport texte, pour éviter de re-parser du texte dans le step suivant - ·Nouveau
scripts/notify_duplicates_telegram.py: lit ce résumé JSON et poste un récap sur le bot Telegram existant ([HIGH]/[REVIEW]+ liste desform_ardes groupes[HIGH]si le message reste sous ~3500 caractères, sinon juste les comptes — marge sous la limite Telegram de 4096) - ·Le workflow upload aussi
report.csv+summary.jsonen artifact (rétention 90j) pour inspection au-delà des logs Actions - ·4 secrets GitHub Actions à ajouter manuellement (Settings → Secrets and variables → Actions, distinct du coffre Vercel) :
SUPABASE_URL,SUPABASE_SERVICE_ROLE_KEY,TELEGRAM_BOT_TOKEN,TELEGRAM_CHAT_ID
v1.9.0 — Juin 2026
- ·Détection de doublons diacritique-insensible : les 2 checks existants (
/ajoutertemps réel +publishContribution.tsavant publication admin) comparaientform_aren égalité stricte SQL (.eq()) — deux graphies du même mot ne différant que par le tachkil (ex:عَلَى خَاطِرْavec sukun final vsعَلَى خَاطِرsans) n'étaient donc jamais détectées comme doublons - ·Nouvelle fonction SQL
normalize_arabic_diacritics(text)(IMMUTABLE, strip tachkil U+064B-U+0670 + tatweel U+0640 + collapse espaces) + RPCfind_duplicate_dialect_form(p_form_ar, p_dialect)— utilisées par les 2 points de contrôle existants à la place du.eq('form_ar', ...)exact (migration appliquée en prod via MCP Supabase) - ·
src/lib/duplicateDetection.ts:normalizeArabicForDup(), miroir TS de la fonction SQL (même classe de caractères quenormalizeSearch()danssearch-utils.ts) — référence partagée, la comparaison réelle se fait côté Postgres via la RPC - ·Nouveau script
scripts/find_duplicates.py(lecture seule) : scannedialect_forms, groupe par(dialect, form_ar normalisé). Découverte en testant sur la prod : le stripping de tachkil seul produit un nombre significatif de faux positifs sur les mots courts — les voyelles internes (fatha/damma/kasra) distinguent souvent 2 mots réellement différents qui partagent le même squelette consonantique non-vocalisé (ex:بَيِّنْ"expliquer" vsبين"entre"). Ajout d'un tag de confiance par groupe ([HIGH]sitranslation_frpartage un mot-clé ≥4 lettres entre 2 entrées du groupe,[REVIEW]sinon) pour rendre le rapport exploitable malgré ce bruit attendu. Option--include-weak(rapport séparé, plus bruyant : mêmetranslation_frnormalisée,form_ardifférent) et--output-csv - ·Testé sur la base prod (2035 lignes
dialect_forms) : 25 groupes de doublons forts détectés, dont 4[HIGH]confirmés comme vrais doublons (ex:مِتْمَرِّس/مْتْمَرِّس,يِشْلِق/يِشْلَق) — aucune écriture, script strictement lecture seule
v1.8.11 — Juin 2026
- ·Fix perf autocomplete
SearchBar(toujours lent après v1.8.10 — confirmé par l'utilisateur : "Chercher" rapide, mais les suggestions sous la barre toujours longues) :autocompleteSearch()(/api/search) enchaînait jusqu'à 4 allers-retours réseau séquentiels (passe RPC fuzzy → passe variantes phonétiques → passe fallback JS fuzzy sur 200 lignes → champs secondaires), chacunawaité seulement après résolution du précédent et quasi-systématiquement déclenché pour une requête courte/partielle (typique pendant la frappe). Vérifié viaEXPLAIN ANALYZEque chaque requête individuelle ne coûte que 1-35ms côté Postgres — le goulot d'étranglement était le nombre de round-trips, pas l'exécution SQL (même diagnostic que v1.8.10, mais sur la route autocomplete plutôt quesearchWords()) - ·Toutes les passes sont désormais lancées en parallèle dès le départ (
Promise.all) au lieu d'être gated séquentiellement surbucket.lengthaprès chaque étape — les passes "fallback" sont toujours récupérées mais leurs résultats ne sont appliqués que si les passes primaires n'ont pas suffi (seul le fetch devient inconditionnel, pas son utilisation) - ·2 index GIN trigram supplémentaires sur expression (
immutable_unaccent(lower(translation_fr)),immutable_unaccent(lower(form_latin))) pour la RPCsearch_df_trad(pill "français" de l'autocomplete), non couverte par les index ajoutés en v1.8.10 (index sur colonne brute, pas sur l'expression unaccent) —Seq Scan35ms →Bitmap Index Scan(BitmapOr) 2ms - ·Testé en local après fix :
/api/search?mode=autocompletewarm à 80-150ms sur les 3 modes (latin/fr/arab), contre plusieurs secondes avant (la chaîne de 4 round-trips séquentiels, chacun ~150-300ms d'overhead réseau/auth/planning PostgREST, expliquait la lenteur perçue malgré des requêtes individuellement rapides)
v1.8.10 — Juin 2026
- ·Fix perf recherche
/dictionnaire(3-4s par recherche signalées) :EXPLAIN ANALYZEa confirmé desSeq Scansurform_ar/translation_fr(40-80ms par requête) — ces colonnes n'avaient que des index BTREE classiques (idx_df_translation_fr,idx_dialect_forms_lower_translation_fr), inutilisables pourILIKE '%...%'(BTREE ne sert qu'au préfixe/égalité, pas "contient"). 5 index GIN trigram ajoutés (form_ar,example,example_fr,fouss7a,translation_frsurdialect_forms) — même requête re-testée après fix :Bitmap Index Scan, 0.3ms (~130x plus rapide) - ·Note honnête : le
Seq Scanlui-même ne prenait que 40-80ms côté Postgres, donc les index seuls n'expliquent pas entièrement 3-4s perçues —searchWords()(db.ts) lance 3 requêtes Supabase par recherche (searchDf+searchMsa+searchDecl), dontsearchDecl()enchaîne 2 ILIKE puis une 3ᵉ requête dépendante, et/dictionnaireajoute un 2ᵉ appel HTTP (?limit=1) au premier chargement — l'empilement de round-trips réseau Supabase est probablement le facteur dominant, les index aident surtout à la scalabilité future - ·Debounce
SearchBar.tsx300ms → 380ms (était déjà à 300, pas 200 comme supposé) - ·Vérifié : pas de scan
ILIKEmulti-colonnes enORdans le chemin principal —field(arab/fr/latin) restreint déjà à une seule colonne primaire, etsecondarySearch()(autocomplete) n'étend déjà aux champs secondaires que si résultats insuffisants. Aucun changement nécessaire sur ce point
v1.8.9 — Juin 2026
- ·Fix critique découvert au premier run production de
import_msa_frequency.py:Insérés en base : 0malgré 210 mots correctement enrichis —dialect_forms_source_checkn'autorisait queadmin|contribution|import_sheets|api|correction, pasai_enriched(niai_generated, déjà utilisé pargemini_generate_msa.py,enrich-words.mjs,generate-msa-words.mjssans qu'aucune ligne n'ait jamais cette valeur en base — ces scripts n'avaient donc probablement jamais réussi à insérer non plus). Contrainte étendue (migrationadd_ai_source_values_to_dialect_forms_source_check) pour inclure les deux valeurs. Aucune donnée corrompue : l'échec était atomique par ligne, 0 écriture partielle avant le fix - ·Quota journalier Groq (
on_demand, 100k tokens/jour) épuisé par inadvertance : le premier run (avant le fix ci-dessus) a consommé ~97k tokens pour enrichir 210 mots qui ont ensuite tous échoué à l'insertion — le coût IA est payé dès l'appel, indépendamment du succès de l'écriture DB. Fenêtre de quota glissante (pas de reset net observé), récupération lente. Un test de validation à 5 mots a confirmé le fix (5 lignes réellement insérées, vérifiées en base) mais a aussi épuisé le reliquat de quota du jour — le run de production suivant (--limit 500) a échoué intégralement (0/500, 34 batches tous en 429 TPD dès le batch 1) - ·État au 2026-06-19 fin de journée : 5 mots MSA importés (
source='ai_enriched') sur 1947 candidats restants (intersection wordfreq ∩ CAMeL, après exclusion des déjà-en-base). Reprise prévue le lendemain avec--provider groq(quota journalier Groq attendu reconstitué)
v1.8.8 — Juin 2026
- ·Nouveau script
scripts/import_msa_frequency.py: importe des mots MSA fiables croisés depuis deux sources de fréquence indépendantes —wordfreq(pip, top 3000) et CAMeL Arabic Frequency Lists MSA (top 3000) — puis enrichit l'intersection via Gemini/Groq (form_latin,translation_fr,pos_type,pattern,pluriel_ar,feminin_ar,example,example_fr) et insère dansdialect_forms(dialect='MSA',source='ai_enriched',source_detail='frequency_crossvalidated',status='validé', structure calquée surgemini_generate_msa.py) - ·Correction de route vs la source annoncée : le repo
CAMeL-Lab/Camel_Arabic_Frequency_Listsne contient aucun fichier.tsvbrut en arborescence (l'URLraw.githubusercontent.com/.../MSA/MSA_freqlist.tsvn'existe pas) — la liste est distribuée via GitHub Releases (MSA_freq_lists.tsv.zip, 69 Mo compressé / 228 Mo / 11.4M lignes). Le script télécharge ce zip une fois (caché dansscripts/.cache/, gitignored) et lit uniquement les 3000 premières lignes du.tsvinterne sans jamais l'extraire en entier sur disque - ·Dédoublonnage contre la base (
words.word_ar+dialect_forms.form_ar WHERE dialect='MSA') insensible aux diacritiques — les entrées générées pargemini_generate_msa.pyportent souvent un tashkeel que les sources de fréquence externes n'ont jamais, un match exact aurait laissé passer de vrais doublons - ·Garde-fou arabe-only sur
pattern/pluriel_ar/feminin_ar(même classe de bug quefouss7av1.8.3) : détecté en dry-run — Groq (llama-3.3) renvoyait parfoispatternen latin (maf3oulau lieu deمفعول) malgré une consigne textuelle ; corrigé en ajoutant un exemple JSON concret en arabe dans le prompt + un rejet automatique (None, loggué) de toute valeur contenant une lettre latine - ·
form_arn'est JAMAIS pris depuis la réponse IA (appariement par index avec la liste source originale uniquement) — le mot est déjà confirmé par double source, on ne fait pas confiance au modèle pour le reproduire fidèlement - ·
--limit(défaut 50),--provider groq|gemini(défaut groq — évite les 503 fréquents de Gemini),--dry-run(lit + enrichit pour prévisualisation, n'écrit rien) - ·Premier dry-run (2026-06-19,
--limit 20) : 2229 mots à l'intersection des deux listes de 3000, 277 déjà en base exclus, 1952 candidats restants, 20/20 enrichis sans erreur — écriture en base non lancée, en attente de validation
v1.8.7 — Juin 2026
- ·Nouveau script
scripts/reconcile_racine_hash.py: réconcilie les racines consonantiques contenant un symbole#(lettre faible élidée non résolue par CamelTools, cf.extract_racine_batch.py) en propageant la version déjà résolue trouvée ailleurs en base pour la même racine (mêmes lettres aux positions non-#, n'importe quelle lettre acceptée à la position#) — débloque le regroupement "Famille de mots" entre mots qui partagent en réalité la même racine - ·Décision par racine
#unique (dédupliquée) : 0 candidat résolu → inchangé, 1 candidat → réconcilié, 2+ candidats différents → ambigu, laissé pour revue manuelle (la plupart des cas ambigus ont 2 positions#simultanées, ex:#.#.ل, donc mécaniquement beaucoup de candidats possibles) - ·Recherche de candidats faite côté Python (un seul fetch de toutes les racines déjà résolues, comparées segment par segment) plutôt que par une requête regex Postgres par racine
#rencontrée — strictement équivalent fonctionnellement, un seul aller-retour réseau au lieu de 200+ - ·Dry-run par défaut (lecture seule),
--apply+ confirmation explicite (input()) requise avant toute écriture, écriture par lots de 50 avec logs de progression - ·État au premier dry-run (2026-06-19) sur 334 mots avec racine
#(225 racines#distinctes) : 45 racines réconciliables (69 mots impactés), 140 ambiguës, 40 sans correspondance - ·Exécuté en base le 2026-06-19 (
--apply, confirmation explicite) : 69 lignes mises à jour, 0 erreur — vérifié (334 → 265 mots restants avec#, écart exact de 69 ; spot-checkزين→ز.ي.ن,فاش→ف.ر.ش)
v1.8.6 — Juin 2026
- ·Fix régression v1.8.5 :
/publier <id>(et/rejeter,/accepter,/refuser,/valider,/annuler) tapé depuis la notif Telegram répondait avec l'aide générique au lieu d'exécuter la commande. Cause : v1.8.5 avait changé le format des notifs de/publier_5a04a(underscore) à/publier 5a04a(espace) — mais l'entité "bot_command" de Telegram ne couvre que[a-zA-Z0-9_]+et s'arrête au premier espace rencontré. Taper sur le lien n'envoyait donc que/publier(ID tronqué/perdu), qui ne matche aucune regex de routage → fallback aide. Reproduit et confirmé par test direct de la regex de routage ("/publier" → NO MATCH) + vérification SQL (la lignecontrib-...-5a04aexistait bien, pending, ILIKE la trouvait correctement — donc le bug était bien côté format de notif/tap, pas côté requête DB) - ·
src/lib/telegram.tsrevient au format underscore (/publier_${shortId}) pour les 3 fonctions de notification concernées — seul format qui survit intact au tap Telegram. Le webhook acceptait déjà les deux formats ([_\s]+), donc la saisie manuelle au clavier avec espace continue de fonctionner - ·Logging de diagnostic ajouté côté
src/app/api/telegram/webhook/route.ts: chaque branche de routage loggue désormais la commande matchée + l'ID extrait (console.info), et les échecs de requête Supabase dans/publieret/rejetersont maintenant loggués enconsole.errorau lieu d'être silencieusement avalés
v1.8.5 — Juin 2026
- ·Fix critique :
analyzeMorphology()(extraction pattern/racine post-soumission, v1.8.4) ne produisait jamais de résultat —pattern_suggested/racine_suggestedrestaient NULL en base sans erreur visible. Cause confirmée par reproduction directe de l'appel Gemini :gemini-2.5-flashréserve par défaut un budget de "thinking" interne qui consommait la quasi-totalité dumaxOutputTokens: 256(thoughtsTokenCount: 242/256observé), tronquant la réponse en plein milieu du JSON (finishReason: MAX_TOKENS) —extractJson()échouait silencieusement (console.warnseulement, jamais remonté). Fix :thinkingConfig: { thinkingBudget: 0 }(tâche d'extraction simple, pas besoin de raisonnement étendu) +maxOutputTokensrelevé à 512 — testé et confirmé en local (finishReason: STOP, JSON complet) - ·Tous les
console.warnsilencieux deanalyzeMorphology.tsremontés enconsole.error(clé API manquante, HTTP non-OK, échec de parsing JSON avec texte brut + finishReason loggés, racine/pattern rejetés) pour rester visibles dans les logs Vercel - ·Normalisation de
racine: Gemini espace parfois les points séparateurs ("ص . ي . ف") — désormais normalisé en"ص.ي.ف"avant stockage, cohérent avec le format utilisé parscripts/extract_racine_batch.py - ·Fix format des commandes Telegram actionnables (
/publier,/rejeter,/valider,/annuler,/accepter,/refuser) :src/lib/telegram.tsenvoyait/publier_5a04a(underscore), désormais/publier 5a04a(espace) — le webhook (src/app/api/telegram/webhook/route.ts) acceptait déjà les deux formats via regex[_\s]+, seul le texte des notifications a changé
v1.8.4 — Juin 2026
- ·Extraction automatique de pattern + racine pour chaque nouvelle contribution via /ajouter, en arrière-plan et non-bloquant pour le contributeur —
analyzeMorphology()(Gemini 2.5 Flash, prompt dédié) déclenché viaafter()(Next.js 15+, exécuté après l'envoi de la réponse, contrairement à un fire-and-forget classique que Vercel peut tuer prématurément) - ·Garde-fou strict (suite au bug fouss7a/translittération de v1.8.3) : tout résultat contenant une lettre latine (
/[a-zA-Z]/) danspatternouracineest intégralement rejeté (aucune écriture en base, aucune notification) — même règle appliquée à l'édition admin de ces champs - ·Nouvelles colonnes
contributions.pattern_suggested/racine_suggested(TEXT) /ia_confidence(FLOAT) — migrationadd_morphology_suggestion_columns_to_contributions - ·Notification Telegram complémentaire silencieuse en cas d'échec/rejet : "🧬 Analyse morphologique automatique" avec schème + racine + % confiance, seulement si l'analyse aboutit
- ·
/adminonglet "En attente" : badge 🧬 Analyse IA (schème + racine + confiance) affiché si présent ; éditable en mode édition avant publication (mêmes garde-fous arabe-only) - ·Publication (
publishContribution.ts) :pattern_suggestedcopié dansdialect_forms.patternseulement si aucun pattern manuel déjà renseigné (ex: import scan OCR prioritaire) ;racine_suggested+ia_confidencecopiés dansdialect_forms.racine/racine_confidence(mêmes colonnes quescripts/extract_racine_batch.py, script inchangé) - ·
/mot/[slug]: racine désormais affichée dans le panneau Morphologie juste après le schème (format lisible "ك - ت - ب"), lecture seule — pas encore de flux de correction dédié (FieldProposalne supporte pas ce champ)
v1.8.3 — Juin 2026
- ·Suppression de l'enrichissement IA automatique (debounce 1500ms) sur /ajouter : coûteux en tokens et source d'un bug de mapping de champ confirmé —
fouss7arecevait parfois la translittération latine au lieu de l'arabe classique (ex: fouss7a="akala" pour "klaa"), le prompt Gemini ne précisant pas explicitement que ce champ doit rester en écriture arabe alors qu'il est injecté juste après leTRANSLIT_BLOCK(entièrement dédié aux règles de romanisation) - ·Tous les champs du formulaire (mot arabe, traduction, translittération, type grammatical, exemple) repassent en saisie 100% manuelle ; mot arabe et traduction française désormais toujours requis (le serveur l'exigeait déjà côté
/api/submit, ce n'était plus reflété côté client une fois l'IA retirée) - ·Champs
pattern/fouss7a/pluriel_ar/feminin_arretirés du formulaire (jamais éditables manuellement, uniquement remplis par l'IA) — restent nullable côté/api/submit - ·Route
/api/enrich-wordconservée intacte (Gemini + fallback Groq) — sera réutilisée pour l'extraction racine/pattern au moment de la publication admin plutôt qu'en saisie utilisateur
v1.8.2 — Juin 2026
- ·Nouvelle section "🌳 Famille de mots" sur /mot/[slug] (distincte de "Même schème", les deux coexistent) : jusqu'à 10 mots partageant exactement la même racine consonantique, triés pattern renseigné d'abord puis dialecte/translittération —
getWordsByRacine()dans db.ts, composantWordFamily.tsx. Racine affichée sous forme lisible ("ك - ت - ب"). Tri "pattern non-null d'abord" fait en JS (stable) car PostgREST ne supporte pas de CASE direct dans.order() - ·
racine/racine_confidenceajoutés au typeWordet ànormalizeDialectForm()(absents jusqu'ici malgré la migration v1.8.1) - ·Fix scripts/extract_racine_batch.py : la validation des racines reposait sur une liste noire incomplète (FOREIGN/NOAN/na) — le placeholder CamelTools "NTWS" ("not a triliteral/templatic stem", typique des emprunts/noms propres) passait au travers, plus quelques racines mal formées sans séparateur (
حل,ل,م,مزل). Remplacé par une validation de forme (regex 2-4 groupes de lettres arabes séparés par des points) — 19 racines invalides corrigées sur re-run complet (1551 → 1532 racines valides, 0 erreur)
v1.8.1 — Juin 2026
- ·Extraction de racine consonantique (3-4 lettres) sur dialect_forms via CamelTools : nouvelles colonnes
racine(déjà migrée) etracine_confidence FLOAT(migration appliquée), scripts/extract_racine_test.py (Phase 1, échantillon 30 mots, lecture seule) puis scripts/extract_racine_batch.py (Phase 2, batch complet) - ·Résolution automatique du symbole "#" (lettre faible élidée détectée par CamelTools) par alignement avec le champ
patternexistant — 177 lettres résolues sur 2022 mots - ·form_ar utilisé en priorité (fouss7a en fallback) — corrige les cas où un fouss7a mal renseigné en base aurait fait dériver une racine depuis le mauvais texte (détecté en Phase 1 sur 2 mots-tests)
- ·Résultat batch final (après fix v1.8.2) : 1532 racines trouvées (1209 confiance 0.9, 323 confiance 0.6), 490 introuvables (racine=NULL), 0 erreur d'écriture
v1.8.0 — Juin 2026
- ·Nouvelle section "Autres mots avec ce schème" sur /mot/[slug] : affiche jusqu'à 8 mots dialectaux partageant exactement le même pattern (schème وزن), cliquables vers leur fiche —
getWordsByPattern()dans db.ts, composantPatternMatches.tsx(style cohérent avec SynonymList) - ·Section masquée automatiquement si le mot n'a pas de pattern ou si aucun autre mot ne le partage
v1.7.16 — Juin 2026
- ·Fix : suggérer une correction du mot arabe renvoyait "Erreur serveur." — la contrainte CHECK Postgres
suggestions_edit_field_checkn'autorisait pas la valeur 'form_ar' (oubli de migration lors de l'ajout du champ en v1.7.15). Contrainte mise à jour en base (translation_fr|pos_type|example|example_fr|form_latin|form_ar)
v1.7.15 — Juin 2026
- ·Mot arabe (form_ar/word_ar) modifiable via le crayon ✏️ sur /mot/[slug] au même titre que la translittération/traduction/exemple/pos_type — nouveau field='form_ar' dans SuggestEdit, /api/suggest-edit, resolveSuggestion.ts
- ·Suggestions de correction du mot arabe acceptables/refusables depuis /admin (onglet Suggestions, affichage RTL dédié) ET depuis Telegram via /accepter_<id> et /refuser_<id> (mécanisme générique déjà en place, aucun changement webhook nécessaire)
- ·Slug jamais régénéré même si le mot arabe change (liens stables, cohérent avec form_latin)
v1.7.14 — Juin 2026
- ·Fix /dictionnaire : le scroll automatique après recherche atterrissait sur les filtres (eux-mêmes sous le clavier mobile resté ouvert) au lieu des résultats. SearchBar.submit() ferme désormais le clavier (blur du champ) à la soumission, et la cible du scroll est déplacée de la section filtres vers la zone de résultats elle-même (bannière d'erreur + grille/état vide/loading)
v1.7.13 — Juin 2026
- ·/dictionnaire : scroll automatique fluide vers la section filtres/résultats à chaque recherche soumise (icône recherche ou Enter), avec scroll-mt pour compenser la Navbar fixe
v1.7.12 — Juin 2026
- ·SearchBar pill (/dictionnaire) : remplacement du bouton "Chercher" texte par une icône Search seule (18px, #C9A84C) dans un bouton rond 36px (tap target 44px), hover rgba(201,168,76,0.15), positionné avant la croix clear
- ·Vérifié : /dictionnaire n'a aucun état hasSearched/isSearched/searchDone — la SearchBar est déjà rendue de façon permanente (acquis depuis v1.7.7), aucune régression trouvée
- ·SearchBar non-pill (page d'accueil) : bouton "Chercher" texte inchangé
v1.7.11 — Juin 2026
- ·SearchBar pill (/dictionnaire) : bouton "Chercher" doré (#C9A84C) à l'intérieur de la barre, visible uniquement quand la query n'est pas vide — texte sur écrans ≥640px, icône Search en dessous
- ·Suppression du tooltip "Astuce : utilisez les chiffres Maghreb" sous la SearchBar (état showHint, helper looksLikeStandardLatin, useEffect associé retirés)
v1.7.10 — Juin 2026
- ·Bot Telegram : commandes /valider et /annuler pour traiter les propositions morphologiques (table corrections — pattern, pluriel_ar, feminin_ar, fouss7a, origine_mot, variants) directement depuis Telegram
- ·Notification proposition morphologique : affiche désormais l'ID court avec les raccourcis /valider et /annuler inline
- ·Logique accept/reject extraite dans src/lib/resolveFieldProposal.ts et partagée entre /api/admin/corrections/[id] et la commande Telegram
v1.7.9 — Juin 2026
- ·Bot Telegram : commandes /accepter et /refuser pour traiter les suggestions d'édition (suggestions_edit) directement depuis Telegram, même mécanique que /publier et /rejeter
- ·Notification suggestion d'édition : affiche désormais l'ID court (6 derniers chars de l'UUID) avec les raccourcis /accepter et /refuser inline
- ·Logique accept/reject des suggestions extraite dans src/lib/resolveSuggestion.ts et partagée entre /api/admin/suggestions/[id] et la commande Telegram
- ·/api/suggest-edit : capture l'id de la ligne insérée (.select('id').single()) pour pouvoir le transmettre à la notification
v1.7.8 — Juin 2026
- ·Bot Telegram : commandes /publier <id> et /rejeter <id> pour modérer les contributions directement depuis Telegram
- ·Notification nouvelle contribution : affiche désormais l'ID court (5 chars) avec les raccourcis /publier et /rejeter inline
- ·Bot Telegram : aide /help mise à jour avec les nouvelles commandes
- ·Fix : tap sur /rejeter <id> dans Telegram n'envoyait que "/rejeter" sans l'ID (l'entité commande Telegram coupe à l'espace) — format /rejeter_<id> sans espace + regex de routage acceptant les deux syntaxes
- ·Fix critique : /publier ne faisait que
contributions.status = 'published'sans jamais insérer dansdialect_forms— le mot n'apparaissait donc jamais dans le dictionnaire. Logique de publication extraite danssrc/lib/publishContribution.ts(slug, doublon, lien concept, revalidation ISR) et partagée entre/api/admin/publishet la commande Telegram /publier
v1.7.7 — Juin 2026
- ·Suppression CAPTCHA arithmétique ("Combien font X + Y ?") de tous les formulaires (SuggestEdit, FieldProposal, VersionForm, UtiliseForm)
- ·Formulaires secondaires (VersionForm, UtiliseForm) : ajout Turnstile avec réutilisation du token de session (hook useTurnstileSession)
- ·/dictionnaire : mise en page permanente — SearchBar pill toujours visible, filtres toujours affichés, suppression de la bascule hero/résultats
- ·Suppression de SearchContext.tsx et de la SearchBar compacte en Navbar
- ·SearchBar : nouvelles props
pill(pill-shaped) etdialect(filtre autocomplete par dialecte actif)
v1.7.6 — Juin 2026
- ·Recherche étendue aux champs secondaires : example, example_fr, fouss7a, pluriel_ar, feminin_ar, pattern
- ·Nouveau type MatchField + matchExcerpt dans la réponse autocomplete (extrait 60 chars autour du match)
- ·6 requêtes ILIKE parallèles pour les champs secondaires, max 3 résultats secondaires par appel
- ·Dropdown SearchBar : extrait souligné en doré (#C9A84C) sous la ligne principale si match secondaire
- ·Mobile first : pills scroll horizontal (overflow-x-auto), suggestions min-h-48px, clear button 36px
- ·Hero dictionnaire : px-4 mobile pour éviter tout débordement sur 390px
v1.7.5 — Juin 2026
- ·Recherche style Google sur /dictionnaire : hero centré au départ, Navbar compacte après recherche
- ·SearchContext (React context) pour synchroniser l'état hero/résultats entre la page et la Navbar
- ·Une seule SearchBar active à la fois : hero (lg, max-w-620px) ou Navbar (sm) selon hasSearched
- ·La Navbar masque la SearchBar sur /dictionnaire tant qu'aucune recherche n'est lancée
- ·Mode par défaut "fr" sur le hero, initField fallback corrigé latin → fr
v1.7.4 — Juin 2026
- ·Double protection anti-bot sur le formulaire /ajouter : Cloudflare Turnstile (principal) + honeypot "website" (complémentaire)
- ·Turnstile affiché une seule fois par session (token stocké en sessionStorage, rechargé en arrière-plan après chaque soumission)
- ·Vérification Turnstile côté serveur (/api/submit) avant toute validation métier
v1.7.3 — Juin 2026
- ·Script de backfill des concepts (scripts/backfill_concepts.ts)
- ·Relie les mots publiés sans concept_id aux concepts existants par mots-clés communs
- ·Modes --dry-run et --limit N ; dry-run sur 50 candidats → 12 liens trouvés (527 ancrages)
v1.7.2 — Juin 2026
- ·Liaison inter-dialectes améliorée à la publication (tryLinkToConcept)
- ·Matching par mots-clés communs au lieu de la traduction exacte (ex : "Énervé, fâché" ↔ "Énervé, fâché, triste, vexé")
- ·Insensible aux accents, scoring par nombre de mots-clés partagés
v1.7.1 — Juin 2026
- ·Fix image OG (Open Graph) : metadataBase pointait sur localhost en production
- ·Les fiches partagées (WhatsApp / Twitter) affichent désormais bien l'image (domaine zeednalearn.com)
v1.7.0 — Juin 2026
- ·Section "Dernières nouveautés" sur la page À propos : timeline des 8 dernières versions
- ·Historique CHANGELOG.md synthétisé (lecture serveur + parsing) + lien vers l'historique complet
v1.6.7 — Juin 2026
- ·Migration des pluriels depuis "Formes du mot" vers la section Morphologie
- ·Suppression du doublon pluriel entre les deux sections
v1.6.6 — Juin 2026
- ·Correction bug HTML entities dans les exemples (apostrophes et guillemets encodés)
- ·Fix racine dans sanitizeText() — plus d'encodage ' en base
v1.6.5 — Juin 2026
- ·Ajout dir="rtl" explicite sur les équivalents inter-dialectes
- ·Garde-fou si form_ar vide sur les synonymes
v1.6.4 — Juin 2026
- ·Revalidation ISR immédiate après actions admin
- ·Les fiches mots sont mises à jour instantanément sans attendre le cache
v1.6.3 — Juin 2026
- ·Crayon contributeurs sur la translittération (form_latin)
- ·Disponible pour tous les dialectes y compris MSA
v1.6.2 — Juin 2026
- ·Fix affichage navbar : pills de mode de recherche déplacées dans le dropdown
v1.6.1 — Juin 2026
- ·Crayon contributeurs sur l'exemple d'utilisation
- ·Formulaire double champ (exemple arabe + traduction FR)
v1.6.0 — Juin 2026
- ·Suggestions d'édition par les contributeurs (traduction, genre grammatical)
- ·Nouveau système de validation admin (onglet 💡 Suggestions)
- ·Notifications Telegram à l'acceptation
v1.5.6 — Juin 2026
- ·Refonte moteur de recherche : generateVariants() produit cartésien
- ·Mapping phonétique : habibi→7abibi, khouya→5ouya, ch↔sh, y↔i, e↔a
- ·Filtre de mode de recherche (Translittéré / Arabe / Français)
v1.5.5 — Juin 2026
- ·Fix recherche : résultats conservés sans sélection de suggestion
- ·Fix filtre dialecte : recherche globale par défaut (plus de TN forcé)
- ·Fix navigation clavier dans les suggestions (flèches haut/bas)
- ·Fix mapping phonétique h→7, kh→5
v1.5.4 — Mai 2026
- ·Bloc morphologique collaboratif (WordMorphology)
- ·Onglet corrections morphologie dans l'admin
- ·Féminin et pluriel visibles sur toutes les fiches (tous pos_type)
v1.5.0 — Mai 2026
- ·Refonte moteur de recherche (normalisation, fuzzy Levenshtein)
- ·Composant SearchBar avec suggestions intelligentes
- ·Index SQL unaccent + pg_trgm
v1.4.5 — Avril 2026
- ·fix(search-bar):
onMouseDown={(e) => e.preventDefault()}sur les boutons suggestion — corrige la race condition onBlur/onClick qui empêchait le clic de firer ; timeout onBlur 150→250ms par sécurité - ·fix(api/search): HTTP 500 sur query contenant une virgule — la virgule était interprétée comme séparateur de conditions dans la syntaxe
.or()de PostgREST ; remplacé par des appels.ilike()parallèles danssearchDecl(),searchDf()(branche arabe) etsearchMsa()(branche arabe) - ·fix(api/search): le catch global retourne désormais HTTP 200
{ suggestions:[], words:[], total:0, error:'search_failed' }au lieu de 500
v1.4.3 — Avril 2026
- ·feat(search): Recherche floue bidirectionnelle Maghreb chiffré ↔ latin standard
- ·Fonction SQL IMMUTABLE
public.normalize_search(text): 7/8→h, 9→q, 5→kh, 3→a, 2→'', aa→a, ou→o, ii→i, Dh→d - ·Index GIN trigram expression sur
dialect_forms.form_latinetwords.word_latin - ·4 RPCs :
search_df_fuzzy,count_df_fuzzy,search_words_fuzzy,count_words_fuzzy - ·
db.ts:searchDf()etsearchMsa()utilisent les RPCs pour les queries latines ; ILIKE direct conservé pour l'arabe et le français - ·
SearchBar.tsx: hint Maghreb chiffré (affiché une seule fois par session si query latin sans chiffres) - ·
scripts/test_search.py: smoke test 7 cas (normalize_search local + RPC live) - ·
npm run test:searchajouté dans package.json
- ·Fonction SQL IMMUTABLE
v1.4.2 — Avril 2026
- ·fix(search): SearchBar utilise
word.slugcomme source de vérité pour les liens suggestions (jamaistransliterationniform_latin) — fix bug clic résultat → mauvaise URL post-migration Maghreb - ·Logique Enter/ArrowUp/ArrowDown refactorisée : mutuellement exclusifs, plus de double-submit
- ·feat(guardrail):
check_translit.pycouvre désormais 4 sections : words, form_latin, variants JSONB, synonyms JSONB (champ transliteration uniquement — les slugs synonymes sont exclus) - ·CLAUDE.md : note explicite slugs ≠ form_latin + règle frontend
v1.4.1 — Avril 2026
- ·Guardrail translittération Maghreb chiffré — 0 régression possible sur les nouveaux inserts
- ·
scripts/maghreb_translit.py: module partagénormalize_translit(text, form_ar)+is_conforming() - ·
scripts/check_translit.py: vérification continue (exit 0 si propre, exit 1 + liste si violations) - ·
src/lib/transliteration.ts: ء → '2' (hamza dialectale) ; ة → 'a' déjà conforme - ·
src/lib/translit.test.ts: +14 castranslateToLatin()couvrant ح/ه/ق/ص/ط/ض/ظ/ة/ء/shadda - ·
src/app/api/enrich-word/route.ts: bloc TRANSLITTÉRATION MAGHREB CHIFFRÉ dans prompts Gemini + Groq - ·
src/app/api/submit/route.ts: guard regex/[āīūṣḥṭḍẓĀĪŪṢḤṬḌẒʿʾ'qQ]/→ 400 si non-conforme - ·
src/app/api/admin/publish/route.ts: idem avant INSERT dialect_forms - ·
scripts/gemini_generate_msa.py: import normalize_translit + fix lowercasing form_latin (préserve S/D/T/Dh) - ·
scripts/gemini_enrich_batch.py: PROMPT_TEMPLATE + Groq system enrichis avec la table Maghreb complète - ·
npm run check:translit+npm run test:translitajoutés dans package.json
v1.4.0 — Avril 2026
- ·Enrichissement IA via Gemini à la saisie des contributions (
/ajouter) - ·Nouveau branding 3amia · AI — hero deux colonnes, calligraphie عامية
- ·Système d'articles avec séries/parties (SerieNav, db enrichi, admin form)
- ·Script batch enrichissement Gemini (
scripts/gemini_enrich_batch.py) - ·Versioning visible dans le footer
v1.3.0 — Mars 2026
- ·Back-office admin complet (contributions, mots publiés, historique)
- ·Notifications Telegram (webhook + import de mots + /stats)
- ·Système de synonymes inter-dialectes (concept_id, table concepts)
- ·Icônes SVG custom (IconSensArtistique, IconPoesie, IconLanguesVivantes…)
- ·Bot Telegram import de mots multi-lignes
v1.2.0 — Mars 2026
- ·Formulaire contribution refonte (pays → région, translittération auto)
- ·Détection doublons temps réel
- ·Déclinaisons morphologiques (table declensions, DeclinaisonsSection)
- ·Enrichissement CamelTools (
scripts/enrich_cameltools.py) - ·Type grammatical pos_type (7 valeurs : ism/fi3l/sifa…)
v1.1.0 — Mars 2026
- ·Migration Google Sheets → Supabase (1 434 mots MSA + 1 771 formes dialectales)
- ·Back-office /admin v1 (authentification Bearer, modération contributions)
- ·SEO generateMetadata sur toutes les pages
- ·Page /a-propos avec stats live Supabase (ISR 1h)
- ·Audit log (table audit_log, logAction helper)
v1.0.0 — Mars 2026
- ·Lancement initial du dictionnaire
- ·3 000+ mots dialectaux (TN, MA, DZ, EG, LB, SY…)
- ·Recherche multilingue (arabe, français, translittération)
- ·Guide phonétique
- ·Formulaire contribution visiteur