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 @@