From 2d8ac663720845c601eb4665d233fc1468693d2c Mon Sep 17 00:00:00 2001 From: IliesAllali Date: Tue, 29 Sep 2026 16:01:16 +0200 Subject: [PATCH] =?UTF-8?q?Site=20en=20anglais=20:=20accueil,=20aide,=20vi?= =?UTF-8?q?e=20priv=C3=A9e,=20guides=20et=20mod=C3=A8les=20sous=20/en/?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Accueil prérendu en deux langues, huit pages statiques traduites avec hreflang des deux côtés, captures de l'aide refaites dans l'app anglaise, modèles PDF générés en français et en anglais, sitemap. Test des liens internes. Co-Authored-By: Claude Opus 5.5 --- landing/index.html | 3 + landing/modeles/gabarits.mjs | 138 ++++++---- landing/modeles/generer.mjs | 29 ++- landing/public/aide/index.html | 5 +- .../application-arbre-genealogique/index.html | 5 +- .../arbre-genealogique-a-remplir/index.html | 5 +- .../arbre-genealogique-avec-photos/index.html | 5 +- .../index.html | 5 +- landing/public/cousinade/index.html | 5 +- .../public/donnees-et-vie-privee/index.html | 5 +- landing/public/en/family-reunion/index.html | 149 +++++++++++ landing/public/en/family-tree-app/index.html | 167 ++++++++++++ .../en/family-tree-with-photos/index.html | 133 ++++++++++ landing/public/en/help/accueil.webp | Bin 0 -> 24426 bytes landing/public/en/help/arbre.webp | Bin 0 -> 26954 bytes landing/public/en/help/envoi.webp | Bin 0 -> 24546 bytes landing/public/en/help/fiche.webp | Bin 0 -> 27488 bytes landing/public/en/help/index.html | 174 +++++++++++++ landing/public/en/help/merci.webp | Bin 0 -> 30492 bytes landing/public/en/help/ouvrir.webp | Bin 0 -> 26780 bytes landing/public/en/help/personne.webp | Bin 0 -> 18706 bytes landing/public/en/help/pour-qui.webp | Bin 0 -> 17662 bytes landing/public/en/help/souvenir.webp | Bin 0 -> 25176 bytes .../en/how-to-make-a-family-tree/index.html | 199 +++++++++++++++ .../family-tree-3-generations.pdf | Bin 0 -> 69113 bytes .../family-tree-3-generations.webp | Bin 0 -> 13730 bytes .../family-tree-4-generations.pdf | Bin 0 -> 77884 bytes .../family-tree-4-generations.webp | Bin 0 -> 14420 bytes .../family-tree-5-generation-fan-chart.pdf | Bin 0 -> 446816 bytes .../family-tree-5-generation-fan-chart.webp | Bin 0 -> 22204 bytes .../family-tree-drawn.pdf | Bin 0 -> 74885 bytes .../family-tree-drawn.webp | Bin 0 -> 18940 bytes .../family-tree-with-photos.pdf | Bin 0 -> 85284 bytes .../family-tree-with-photos.webp | Bin 0 -> 17212 bytes .../en/printable-family-tree/index.html | 208 +++++++++++++++ landing/public/en/privacy/index.html | 133 ++++++++++ .../en/tell-your-family-story/index.html | 139 ++++++++++ .../raconter-histoire-de-famille/index.html | 5 +- landing/public/sitemap.xml | 54 ++++ landing/scripts/localize-head.mjs | 31 +++ landing/scripts/prerender.mjs | 29 ++- landing/src/App.jsx | 189 ++++++-------- landing/src/entry-server.jsx | 7 +- landing/src/i18n.js | 240 ++++++++++++++++++ landing/src/main.jsx | 3 +- landing/test/i18n.test.mjs | 71 ++++++ landing/test/liens-internes.test.mjs | 46 ++++ 47 files changed, 1996 insertions(+), 186 deletions(-) create mode 100644 landing/public/en/family-reunion/index.html create mode 100644 landing/public/en/family-tree-app/index.html create mode 100644 landing/public/en/family-tree-with-photos/index.html create mode 100644 landing/public/en/help/accueil.webp create mode 100644 landing/public/en/help/arbre.webp create mode 100644 landing/public/en/help/envoi.webp create mode 100644 landing/public/en/help/fiche.webp create mode 100644 landing/public/en/help/index.html create mode 100644 landing/public/en/help/merci.webp create mode 100644 landing/public/en/help/ouvrir.webp create mode 100644 landing/public/en/help/personne.webp create mode 100644 landing/public/en/help/pour-qui.webp create mode 100644 landing/public/en/help/souvenir.webp create mode 100644 landing/public/en/how-to-make-a-family-tree/index.html create mode 100644 landing/public/en/printable-family-tree/family-tree-3-generations.pdf create mode 100644 landing/public/en/printable-family-tree/family-tree-3-generations.webp create mode 100644 landing/public/en/printable-family-tree/family-tree-4-generations.pdf create mode 100644 landing/public/en/printable-family-tree/family-tree-4-generations.webp create mode 100644 landing/public/en/printable-family-tree/family-tree-5-generation-fan-chart.pdf create mode 100644 landing/public/en/printable-family-tree/family-tree-5-generation-fan-chart.webp create mode 100644 landing/public/en/printable-family-tree/family-tree-drawn.pdf create mode 100644 landing/public/en/printable-family-tree/family-tree-drawn.webp create mode 100644 landing/public/en/printable-family-tree/family-tree-with-photos.pdf create mode 100644 landing/public/en/printable-family-tree/family-tree-with-photos.webp create mode 100644 landing/public/en/printable-family-tree/index.html create mode 100644 landing/public/en/privacy/index.html create mode 100644 landing/public/en/tell-your-family-story/index.html create mode 100644 landing/scripts/localize-head.mjs create mode 100644 landing/src/i18n.js create mode 100644 landing/test/i18n.test.mjs create mode 100644 landing/test/liens-internes.test.mjs diff --git a/landing/index.html b/landing/index.html index b370665..c44d864 100644 --- a/landing/index.html +++ b/landing/index.html @@ -10,6 +10,9 @@ Parenthèse : l'arbre généalogique vivant de votre famille + + + diff --git a/landing/modeles/gabarits.mjs b/landing/modeles/gabarits.mjs index 4ab40d5..cd542e6 100644 --- a/landing/modeles/gabarits.mjs +++ b/landing/modeles/gabarits.mjs @@ -22,6 +22,55 @@ const LOGO_RATIO = 7.541 const esc = (s) => String(s).replace(/&/g, '&').replace(/ Math.round(v * 100) / 100 +// Libellés imprimés sur les modèles, par langue (fr = page /arbre-genealogique-a-remplir/, +// en = page /en/printable-family-tree/) +export const TEXTES = { + fr: { + titre: ['Arbre ', 'généalogique'], + famille: 'de la famille', + pied: 'Modèle gratuit à imprimer et à remplir', + photo: 'Photo', + roles: [ + ['Moi'], + ['Père', 'Mère'], + ['Grand-père paternel', 'Grand-mère paternelle', 'Grand-père maternel', 'Grand-mère maternelle'], + ['Arrière-grand-père', 'Arrière-grand-mère'], + ], + champs: ['Prénom', 'Nom', 'Né(e) le', 'à', 'Décès'], + legendes: [['Moi'], ['Parents'], ['Grands-', 'parents'], ['Arrière-grands-', 'parents'], ['Trisaïeuls']], + sosa: "Numérotation Sosa. Le père d'une personne porte le double de son numéro, la mère le double plus un.", + tags: { + g3: '3 générations', + g4: '4 générations', + photos: '3 générations avec photos', + eventail: '5 générations en éventail', + dessine: '3 générations, arbre dessiné', + }, + }, + en: { + titre: ['Family ', 'tree'], + famille: 'of the family', + pied: 'Free template to print and fill in', + photo: 'Photo', + roles: [ + ['Me'], + ['Father', 'Mother'], + ['Paternal grandfather', 'Paternal grandmother', 'Maternal grandfather', 'Maternal grandmother'], + ['Great-grandfather', 'Great-grandmother'], + ], + champs: ['First name', 'Last name', 'Born on', 'in', 'Died'], + legendes: [['Me'], ['Parents'], ['Grand-', 'parents'], ['Great-', 'grandparents'], ['Great-great-', 'grandparents']], + sosa: "Ahnentafel (Sosa) numbering. A person's father has double their number, their mother double plus one.", + tags: { + g3: '3 generations', + g4: '4 generations', + photos: '3 generations with photos', + eventail: '5-generation fan chart', + dessine: '3 generations, drawn tree', + }, + }, +} + const STYLE = ` .titre { font-family: 'Newsreader', serif; font-size: 9px; fill: ${C.encre}; } .titre-i { font-family: 'Newsreader', serif; font-style: italic; font-size: 9px; fill: ${C.a700}; } @@ -40,10 +89,10 @@ const STYLE = ` .photo { fill: #fff; stroke: ${C.encre}; stroke-opacity: .28; stroke-width: .2; stroke-dasharray: .8 .8; } ` -function page({ W, H, body, titre = 'modèle' }) { +function page({ W, H, body, titre = 'modèle', lang = 'fr' }) { const orient = W > H ? 'landscape' : 'portrait' return ` -${esc(titre)} +${esc(titre)} @@ -65,35 +114,31 @@ function champ(x, xEnd, y, label) { return `${esc(label)}` } -function entete({ W, margin, tag, top = 14 }) { +function entete({ W, margin, tag, t, top = 14 }) { const y = top + 9 return ` - Arbre généalogique - de la famille + ${esc(t.titre[0])}${esc(t.titre[1])} + ${esc(t.famille)} ${esc(tag)} ` } -function pied({ W, H, margin, texte = 'Modèle gratuit à imprimer et à remplir' }) { +function pied({ W, H, margin, t }) { const h = 3.4 const w = h * LOGO_RATIO const y = H - 8 return ` ${LOGO_INNER} - ${esc(texte)}, parenthese.io` + ${esc(t.pied)}, parenthese.io` } -const ROLES = [ - ['Moi'], - ['Père', 'Mère'], - ['Grand-père paternel', 'Grand-mère paternelle', 'Grand-père maternel', 'Grand-mère maternelle'], - Array.from({ length: 8 }, (_, i) => (i % 2 ? 'Arrière-grand-mère' : 'Arrière-grand-père')), -] +// Rôle de chaque case, génération par génération (les arrière-grands-parents alternent père, mère) +const rolesDe = (t) => [t.roles[0], t.roles[1], t.roles[2], Array.from({ length: 8 }, (_, i) => t.roles[3][i % 2])] -const CHAMPS = ['Prénom', 'Nom', 'Né(e) le', 'à', 'Décès'] +const NB_CHAMPS = 5 // Boîte de personne : rôle + numéro Sosa en tête, photo optionnelle, puis les champs -function boite({ x, y, w, h, role, sosa, ls, photo }) { +function boite({ x, y, w, h, role, sosa, ls, photo, t }) { const pad = 2.4 let out = `` out += `${esc(role)}` @@ -104,21 +149,23 @@ function boite({ x, y, w, h, role, sosa, ls, photo }) { const ph = photo.h const px = x + (w - pw) / 2 out += `` - out += `Photo` + out += `${esc(t.photo)}` cy += ph + 1.6 } - CHAMPS.forEach((label, k) => { + t.champs.forEach((label, k) => { out += champ(x + pad, x + w - pad, cy + (k + 1) * ls - 0.9, label) }) return out } function hauteurBoite(ls, photo) { - return 6 + (photo ? photo.h + 1.6 : 0) + CHAMPS.length * ls + 1.4 + return 6 + (photo ? photo.h + 1.6 : 0) + NB_CHAMPS * ls + 1.4 } // Arbre ascendant vertical : « Moi » en bas, les ancêtres au-dessus, côté paternel à gauche -function arbre({ W, H, margin, top, bottom, gens, gap, maxW, ls, photo, tag, texte }) { +function arbre({ W, H, margin, top, bottom, gens, gap, maxW, ls, photo, tag, lang }) { + const t = TEXTES[lang] + const ROLES = rolesDe(t) const boxH = hauteurBoite(ls, photo) const vgap = (bottom - top - gens * boxH) / (gens - 1) const rowY = (g) => bottom - boxH - g * (boxH + vgap) @@ -131,7 +178,7 @@ function arbre({ W, H, margin, top, bottom, gens, gap, maxW, ls, photo, tag, tex const slot = availW / count const w = Math.min(slot - gap, maxW[g]) for (let i = 0; i < count; i++) { - boites += boite({ x: cx(g, i) - w / 2, y: rowY(g), w, h: boxH, role: ROLES[g][i], sosa: count + i, ls, photo }) + boites += boite({ x: cx(g, i) - w / 2, y: rowY(g), w, h: boxH, role: ROLES[g][i], sosa: count + i, ls, photo, t }) if (g < gens - 1) { const yb = rowY(g + 1) + boxH const yt = rowY(g) @@ -142,27 +189,27 @@ function arbre({ W, H, margin, top, bottom, gens, gap, maxW, ls, photo, tag, tex } } } - return page({ W, H, body: entete({ W, margin, tag }) + traits + boites + pied({ W, H, margin, texte }) }) + return page({ W, H, lang, body: entete({ W, margin, tag: t.tags[tag], t }) + traits + boites + pied({ W, H, margin, t }) }) } -export function troisGenerations() { +export function troisGenerations(lang = 'fr') { return arbre({ W: 210, H: 297, margin: 14, top: 50, bottom: 276, gens: 3, gap: 4, - maxW: [84, 66, 42], ls: 8.2, tag: '3 générations', + maxW: [84, 66, 42], ls: 8.2, tag: 'g3', lang, }) } -export function quatreGenerations() { +export function quatreGenerations(lang = 'fr') { return arbre({ W: 297, H: 210, margin: 12, top: 47, bottom: 193, gens: 4, gap: 2.6, - maxW: [76, 64, 56, 32], ls: 5.3, tag: '4 générations', + maxW: [76, 64, 56, 32], ls: 5.3, tag: 'g4', lang, }) } -export function troisGenerationsPhotos() { +export function troisGenerationsPhotos(lang = 'fr') { return arbre({ W: 210, H: 297, margin: 14, top: 46, bottom: 280, gens: 3, gap: 4, - maxW: [84, 66, 42], ls: 6.2, photo: { w: 23, h: 28 }, tag: '3 générations avec photos', + maxW: [84, 66, 42], ls: 6.2, photo: { w: 23, h: 28 }, tag: 'photos', lang, }) } @@ -187,14 +234,15 @@ function arc(cx, cy, r, d0, d1) { return `M${n(x0)} ${n(y0)} A${r} ${r} 0 0 1 ${n(x1)} ${n(y1)}` } -export function cinqGenerationsEventail() { +export function cinqGenerationsEventail(lang = 'fr') { + const t = TEXTES[lang] const W = 297 const H = 210 const margin = 12 const cx = W / 2 const cy = 188 const R = [0, 26, 55, 82, 108, 134] - const LEGENDES = [['Moi'], ['Parents'], ['Grands-', 'parents'], ['Arrière-grands-', 'parents'], ['Trisaïeuls']] + const LEGENDES = t.legendes let body = '' // anneaux de la génération 2 à 5 (Sosa 2 à 31), du père (gauche) à la mère (droite) for (let g = 1; g <= 4; g++) { @@ -240,13 +288,13 @@ export function cinqGenerationsEventail() { const mid = g === 0 ? 0 : (R[g] + R[g + 1]) / 2 const xs = g === 0 ? [cx] : [cx - mid, cx + mid] xs.forEach((x) => { - lines.forEach((t, k) => { - body += `${esc(t)}` + lines.forEach((ligne, k) => { + body += `${esc(ligne)}` }) }) }) - body += `Numérotation Sosa. Le père d'une personne porte le double de son numéro, la mère le double plus un.` - return page({ W, H, body: entete({ W, margin, tag: '5 générations en éventail' }) + body + pied({ W, H, margin }) }) + body += `${esc(t.sosa)}` + return page({ W, H, lang, body: entete({ W, margin, tag: t.tags.eventail, t }) + body + pied({ W, H, margin, t }) }) } // ─── Arbre dessiné, 3 générations ─────────────────────────────────────────── @@ -276,7 +324,8 @@ function feuille(x, y, angle, s) { return `` } -export function arbreDessine() { +export function arbreDessine(lang = 'fr') { + const t = TEXTES[lang] const W = 210 const H = 297 const margin = 14 @@ -322,23 +371,24 @@ export function arbreDessine() { // sol dessin += `` let boites = '' - const roles = [ROLES[0], ROLES[1], ROLES[2]] + const roles = rolesDe(t) ;[[moi], parents, gps].forEach((row, g) => { row.forEach((b, i) => { - boites += boite({ x: b.x - b.w / 2, y: b.y, w: b.w, h: bh, role: roles[g][i], sosa: 2 ** g + i, ls }) + boites += boite({ x: b.x - b.w / 2, y: b.y, w: b.w, h: bh, role: roles[g][i], sosa: 2 ** g + i, ls, t }) }) }) // teintes opaques (a300 à 55 % et a500 à 50 % sur blanc) pour éviter les surimpressions aux jonctions - const body = entete({ W, margin, tag: '3 générations, arbre dessiné' }) + const body = entete({ W, margin, tag: t.tags.dessine, t }) + `${dessin}${feuillage}` - + boites + pied({ W, H, margin }) - return page({ W, H, body }) + + boites + pied({ W, H, margin, t }) + return page({ W, H, lang, body }) } +// slug = nom des fichiers français, en = nom des fichiers anglais export const MODELES = [ - { slug: 'arbre-genealogique-3-generations', build: troisGenerations, portrait: true }, - { slug: 'arbre-genealogique-4-generations', build: quatreGenerations, portrait: false }, - { slug: 'arbre-genealogique-5-generations-eventail', build: cinqGenerationsEventail, portrait: false }, - { slug: 'arbre-genealogique-avec-photos', build: troisGenerationsPhotos, portrait: true }, - { slug: 'arbre-genealogique-dessine', build: arbreDessine, portrait: true }, + { slug: 'arbre-genealogique-3-generations', en: 'family-tree-3-generations', build: troisGenerations, portrait: true }, + { slug: 'arbre-genealogique-4-generations', en: 'family-tree-4-generations', build: quatreGenerations, portrait: false }, + { slug: 'arbre-genealogique-5-generations-eventail', en: 'family-tree-5-generation-fan-chart', build: cinqGenerationsEventail, portrait: false }, + { slug: 'arbre-genealogique-avec-photos', en: 'family-tree-with-photos', build: troisGenerationsPhotos, portrait: true }, + { slug: 'arbre-genealogique-dessine', en: 'family-tree-drawn', build: arbreDessine, portrait: true }, ] diff --git a/landing/modeles/generer.mjs b/landing/modeles/generer.mjs index 97f4c4d..8e4fc74 100644 --- a/landing/modeles/generer.mjs +++ b/landing/modeles/generer.mjs @@ -1,6 +1,8 @@ -// Génère les modèles PDF A4 et leurs aperçus WebP pour /arbre-genealogique-a-remplir/. +// Génère les modèles PDF A4 et leurs aperçus WebP pour /arbre-genealogique-a-remplir/ (français) +// et /en/printable-family-tree/ (anglais). // -// node modeles/generer.mjs (depuis landing/) +// node modeles/generer.mjs (depuis landing/, les deux langues) +// node modeles/generer.mjs dessine (seulement les modèles dont le nom de fichier contient « dessine ») // // Playwright n'est pas une dépendance de la landing : le script le cherche dans node_modules, // puis dans le dossier indiqué par PLAYWRIGHT_DIR (ex. D:/Commercial/tools/web-qa/node_modules). @@ -22,7 +24,11 @@ function chargerPlaywright() { throw new Error('Playwright introuvable. Définir PLAYWRIGHT_DIR vers un node_modules qui le contient.') } -const OUT = fileURLToPath(new URL('../public/arbre-genealogique-a-remplir/', import.meta.url)) +// Une sortie par langue : dossier de la page et nom de fichier de chaque modèle +const LANGUES = [ + { lang: 'fr', out: fileURLToPath(new URL('../public/arbre-genealogique-a-remplir/', import.meta.url)), nom: (m) => m.slug }, + { lang: 'en', out: fileURLToPath(new URL('../public/en/printable-family-tree/', import.meta.url)), nom: (m) => m.en }, +] const APERCU_LARGEUR = 720 // Recale chaque trait d'écriture juste après son libellé, une fois les polices chargées @@ -37,16 +43,17 @@ function ajusterTraits() { async function main() { const { chromium } = chargerPlaywright() - mkdirSync(OUT, { recursive: true }) const browser = await chromium.launch({ channel: 'chrome' }) const ctx = await browser.newContext({ deviceScaleFactor: 2 }) const page = await ctx.newPage() const convert = await ctx.newPage() const seul = process.argv[2] - for (const m of MODELES) { - if (seul && !m.slug.includes(seul)) continue - const html = m.build() + for (const { lang, out, nom } of LANGUES) for (const m of MODELES) { + const slug = nom(m) + if (seul && !slug.includes(seul)) continue + mkdirSync(out, { recursive: true }) + const html = m.build(lang) const [wmm, hmm] = m.portrait ? [210, 297] : [297, 210] await page.setViewportSize({ width: Math.ceil(wmm * 96 / 25.4), height: Math.ceil(hmm * 96 / 25.4) }) await page.setContent(html, { waitUntil: 'networkidle' }) @@ -60,10 +67,10 @@ async function main() { await document.fonts.ready return document.fonts.check('400 12px "DM Sans"') && document.fonts.check('400 12px "Newsreader"') }) - if (!ok) throw new Error(`Polices non chargées pour ${m.slug}`) + if (!ok) throw new Error(`Polices non chargées pour ${slug}`) await page.evaluate(ajusterTraits) - const pdf = `${OUT}${m.slug}.pdf` + const pdf = `${out}${slug}.pdf` await page.pdf({ path: pdf, preferCSSPageSize: true, printBackground: true }) // Aperçu : capture de la feuille, réduite et encodée en WebP par le canvas de Chromium @@ -81,8 +88,8 @@ async function main() { return { url: c.toDataURL('image/webp', 0.86), w: c.width, h: c.height } }, { b64: png.toString('base64'), largeur: APERCU_LARGEUR }) const webp = Buffer.from(dataUrl.url.split(',')[1], 'base64') - writeFileSync(`${OUT}${m.slug}.webp`, webp) - console.log(`${m.slug} pdf ok apercu ${dataUrl.w}x${dataUrl.h} ${Math.round(webp.length / 1024)} Ko`) + writeFileSync(`${out}${slug}.webp`, webp) + console.log(`${lang} ${slug} pdf ok apercu ${dataUrl.w}x${dataUrl.h} ${Math.round(webp.length / 1024)} Ko`) } await browser.close() } diff --git a/landing/public/aide/index.html b/landing/public/aide/index.html index 6b8d65c..8a11e1c 100644 --- a/landing/public/aide/index.html +++ b/landing/public/aide/index.html @@ -7,6 +7,9 @@ Vous avez reçu le lien de l'arbre de votre famille, le mode d'emploi + + + @@ -156,7 +159,7 @@