Aller au contenu principal
Recette numéro 178

Recettes · Chapitre 10 · Sortie et intégration

Une maquette pour trois éditions en trois langues

Un livret en anglais, en espagnol et en arabe tiré de réglages généraux, avec une surcharge pour ce qui revient à chaque langue : polices, corps, interlettrage.

Sur cette page
Sortie
Canvas · PDF
Niveau
Avancé
Postext
Testé avec Postext 1.65.0
Nécessite ≥ 1.65.0 · postext-pdf ≥ 1.65.0
Licence
Mis à jour le 31 oct. 2026
Code MIT · Texte CC BY 4.0
  • Exemple en anglais : pas encore d’édition française
  • Format 148 × 210 mm
  • 1 colonne
  • Source Serif 4 10/14
  • Source Sans 3
  • 4 pages
  • Niveau
  • Postext 1.65.0
  • Mis en page en 223 ms
  • 205 lignes de code

En bref

Un livret sur trois astrolabes anciens, imprimé en anglais, en espagnol et en arabe. Les trois partagent une maquette ; seule l’édition arabe change ses polices et ses corps.

Ce que vous allez composer

Un livret de quatre pages A5 sur trois astrolabes du Metropolitan Museum of Art : l’un fait au Yémen en 1291, l’autre en Iran en 1654, le dernier en Europe en 1462. L’éditeur le publie en anglais, en espagnol et en arabe, et les trois éditions sont une seule maquette. La photo de couverture, les intertitres couleur laiton, l’encadré et son filet, les figures habillées par le texte et l’en-tête sombre du tableau viennent des mêmes réglages. L’édition arabe change ce qui tient à son écriture : Amiri et IBM Plex Sans Arabic au lieu de Source Serif et Source Sans, un corps plus grand et des libellés sans capitales ni interlettrage. Elle se lit depuis la droite et se relie à droite sans une seule coordonnée à elle.

Cette recette répond à

  • Comment partager une maquette entre les éditions d’un livre en plusieurs langues et ne changer que les polices, la césure, les guillemets et les libellés de chaque langue ?
  • Comment publier le même livre en deux langues à partir d’un seul projet ?

La réponse courte

script.js · lignes 114–141dans le code complet
const ARABIC_LABEL = { fontFamily: 'IBM Plex Sans Arabic', letterSpacing: pt(0),
  textTransform: 'none' }; // Arabic letters join: never tracked, and they have no capitals
// English and Spanish need nothing more than their locale: it picks the hyphenation patterns
// and the words of the figure and table labels. Arabic sets the faces of its script.
const overrides = {
  ar: {
    bodyText: { fontFamily: 'Amiri', fontSize: pt(12), lineHeight: pt(19),
      firstLineIndent: em(1.5) },
    headings: { fontFamily: 'Amiri', fontWeight: 700,
      levels: [{ level: 2, fontSize: pt(15), lineHeight: pt(24) }] },
    // Lists keyed by id merge entry by entry: only these fields of these elements change.
    headingStyles: [{ id: 'cover', advancedDesign: { slot: { elements: [
      { id: 'kicker', ...ARABIC_LABEL, fontSize: pt(10) },
      { id: 'title', fontFamily: 'Amiri', fontWeight: 700, fontSize: pt(50), lineHeight: 1.25 },
      { id: 'facts', ...ARABIC_LABEL, fontSize: pt(9.5) },
    ] } } }],
    header: { elements: ['verso-folio', 'verso-title', 'recto-title', 'recto-folio']
      .map((id) => ({ id, ...ARABIC_LABEL, fontSize: pt(9) })) },
    calloutStyles: [{ id: 'note', titleStyle: { ...ARABIC_LABEL, fontSize: pt(10) },
      body: { fontSize: pt(11.5), lineHeight: pt(19) } }],
    tableStyle: { headerFontFamily: 'IBM Plex Sans Arabic', headerFontSize: pt(9),
      bodyFontFamily: 'IBM Plex Sans Arabic', bodyFontSize: pt(9.4) },
    captionStyle: { fontFamily: 'IBM Plex Sans Arabic', fontSize: pt(9) },
    paragraphStyles: [{ id: 'colophon', fontFamily: 'IBM Plex Sans Arabic', fontSize: pt(8.4),
      lineHeight: pt(13) }],
  },
};
const edition = mergeConfigLayers(config, overrides[LANG]); // config itself for en and es

Ingrédients

Typographie
Source Serif 4, Source Sans 3, Amiri, IBM Plex Sans Arabic (SIL OFL 1.1)
Ressources
  • mashhad-800.jpg
  • rasulid-cover-1400.jpg
  • regiomontanus-800.jpg
  • Astrolabe of ʿUmar ibn Yusuf, Yemen, 1291 (the cover; background extended at the top and darkened) (The Metropolitan Museum of Art, CC0 1.0)
  • Muhammad Zaman, planispheric astrolabe, Iran, 1654–55 (cropped) (The Metropolitan Museum of Art, CC0 1.0)
  • Astrolabe attributed to Regiomontanus, 1462 (cropped) (The Metropolitan Museum of Art, CC0 1.0)

Préparation

#1 · Des réglages généraux et trois petites surcharges

Le code est dans la réponse courte plus haut. config contient les réglages généraux ; mergeConfigLayers pose par-dessus la surcharge d’une édition, champ par champ. L’anglais et l’espagnol n’ont pas de surcharge : leur locale choisit les motifs de césure et les mots Figure et Table, Figura et Tabla. La surcharge arabe ne nomme que des champs : bodyText.fontFamily change la police et garde la couleur, l’alignement et la césure, et l’élément kicker de la couverture change sa police et son interlettrage et garde son ancre, sa couleur et sa taille. Un changement de palette ou de marges dans config atteint les trois éditions.

#2 · Des côtés nommés selon le sens du texte

script.js · lignes 59–72dans le code complet
// An even page's outer edge is its start side in either direction (the left of an English
// verso, the right of an Arabic one), an odd page's its end side.
const head = (id, content, parity, edge, x, extra = {}) => ({ kind: 'text', id, content,
  parity, pages: 'body', fontFamily: LABEL, fontSize: pt(7.5), fontWeight: 600,
  letterSpacing: pt(1.1), textTransform: 'uppercase', color: col('muted'),
  align: edge.endsWith('start') ? 'start' : 'end',
  placement: { anchor: { to: 'page', edge }, offset: { x: mm(x), y: mm(12) } }, ...extra });
const folio = { fontWeight: 700, color: col('brass') };
const header = { elements: [
  head('verso-folio', '{pageNumber}', 'even', 'top-start', 16, folio),
  head('verso-title', '{title}', 'even', 'top-start', 24),
  head('recto-title', '{subtitle}', 'odd', 'top-end', -24),
  head('recto-folio', '{pageNumber}', 'odd', 'top-end', -16, folio),
] };

Le bord extérieur d’une page paire est le début de ses lignes dans les deux sens, celui d’une page impaire leur fin ; top-start et top-end mettent donc les folios à l’extérieur sur un verso anglais comme sur un verso arabe. Le filet de l’encadré est side: 'start' et le texte de couverture s’ancre à top-start : aucun n’a besoin de surcharge.

#3 · Des figures en fin de ligne, habillées par le texte

script.js · lignes 195–219dans le code complet
const photo = (id, file, w, h, extra = {}) => ({ id, typeId: 'figure', kind: 'bitmap',
  createdAt: 0, updatedAt: 0, ...extra,
  bitmap: { fileId: file, format: 'jpeg', width: w, height: h } });
// 'end' is the right of an English column and the left of an Arabic one.
const wrapped = { position: 'here', wrap: 'end', width: 0.38 };
const note = (n) => `The Met, ${n}, CC0`;
const resources = [
  photo('rasulid', 'rasulid-cover-1400.jpg', 1400, 1985),
  photo('mashhad', 'mashhad-800.jpg', 800, 1187, { placement: wrapped, note: note('63.166a–j'),
    caption: t({ en: 'Planispheric astrolabe, Iran, 1654–55. Brass and steel.',
      es: 'Astrolabio planisférico, Irán, 1654-1655. Latón y acero.',
      ar: 'أسطرلاب مسطّح، إيران، ١٠٦٥\u00a0هـ / ١٦٥٤–١٦٥٥\u00a0م. نحاس أصفر وفولاذ.' }),
    altText: t({ en: 'A brass astrolabe whose rete is a lattice of leaves and scrolls',
      es: 'Un astrolabio de latón cuya red es un enrejado de hojas y volutas',
      ar: 'أسطرلاب من النحاس الأصفر عنكبوته شبكة من الأوراق والأغصان الملتفّة' }) }),
  photo('regiomontanus', 'regiomontanus-800.jpg', 800, 1083,
    { placement: wrapped,
    note: note('2025.347a–j'),
    caption: t({ en: 'Astrolabe attributed to Regiomontanus, 1462. Copper alloy.',
      es: 'Astrolabio atribuido a Regiomontano, 1462. Aleación de cobre.',
      ar: 'أسطرلاب يُنسب إلى ريجيومونتانوس، ١٤٦٢\u00a0م. سبيكة نحاسية.' }),
    altText: t({ en: 'A round brass astrolabe with Latin star names on its rete',
      es: 'Un astrolabio redondo de latón con nombres latinos de estrellas en la red',
      ar: 'أسطرلاب مستدير من النحاس عليه أسماء نجوم لاتينية' }) }),
];

wrap: 'end' place chaque photographie à droite d’une colonne anglaise et à gauche d’une colonne arabe, et le texte court à côté. Légendes et textes alternatifs sont dans la langue de chaque édition ; les légendes arabes donnent d’abord la date de l’hégire.

#4 · Le tableau avec les mots et les chiffres de chaque édition

script.js · lignes 223–241dans le code complet
const row = (...cells) => cells.map((content) => ({ content }));
const head2 = (...cells) => cells.map((content) => ({ content, isHeader: true }));
resources.push({ id: 'three', typeId: 'table', kind: 'table', createdAt: 0, updatedAt: 0,
  placement: { position: 'here' },
  caption: t({ en: 'The three instruments.', es: 'Los tres instrumentos.',
    ar: 'الأدوات الثلاث.' }),
  table: { model: { headerRowCount: 1, columnWidths: [1.15, 1.1, 1.2, 1.1], rows: t({
    en: [head2('Made', 'Where', 'Maker', 'Size'),
      row('1291', 'Yemen', 'ʿUmar ibn Yūsuf', '15.6 cm wide'),
      row('1654–55', 'Iran, Mashhad?', 'Muhammad Zaman', '21.6 cm high'),
      row('1462', 'Germany or Italy', 'Regiomontanus', '13.5 cm high')],
    es: [head2('Fecha', 'Lugar', 'Autor', 'Tamaño'),
      row('1291', 'Yemen', 'ʿUmar ibn Yūsuf', '15,6 cm de ancho'),
      row('1654-1655', 'Irán, ¿Mashhad?', 'Muhammad Zaman', '21,6 cm de alto'),
      row('1462', 'Alemania o Italia', 'Regiomontano', '13,5 cm de alto')],
    ar: [head2('التاريخ', 'المكان', 'الصانع', 'المقاس'),
      row('٦٩٠\u00a0هـ / ١٢٩١\u00a0م', 'اليمن', 'عمر بن يوسف', 'عرضه ١٥٫٦\u00a0سم'),
      row('١٠٦٥\u00a0هـ / ١٦٥٤\u00a0م', 'إيران، مشهد؟', 'محمد زمان', 'ارتفاعه ٢١٫٦\u00a0سم'),
      row('١٤٦٢\u00a0م', 'ألمانيا أو إيطاليا', 'ريجيومونتانوس', 'ارتفاعه ١٣٫٥\u00a0سم')] }) } } });

Les données du tableau sont du contenu et changent avec l’édition : chiffres arabes orientaux et séparateur décimal arabe dans l’édition arabe, virgule décimale dans l’espagnole. Son style est général : la surcharge ne fixe que la police et le corps arabes.

La recette complète

Sandbox
// ═══ Postext Cookbook · Nº 178 · One design for three language editions ═══════════
// https://postext.dev/en/cookbook/shared-design-per-language
// Code: MIT · Text: original (CC BY 4.0) · Photographs: The Met, Open Access (CC0)
// Fonts: Source Serif 4, Source Sans 3, Amiri, IBM Plex Sans Arabic (OFL) · Needs postext ≥ 1.65.0
import {
  buildDocument, withLoadedFonts, renderPageToCanvas, registerResourceImage, mergeConfigLayers,
} from 'https://esm.sh/postext';
import { renderToPdf, decompressWoff2 } from 'https://esm.sh/postext-pdf';

const LANG = 'en'; // @lang: the edition: 'en' | 'es' | 'ar'
const RECIPE = 'shared-design-per-language';

// ─── 1 · Design ─────────────────────────────────────────────────────────────
// #region palette: night ink, the brass of the instruments, a parchment tint
const palette = {
  ink: '#1f2230', // text: a blue near-black
  brass: '#80561c', // the one accent: kickers, rules, the box stripe (6.2:1 on paper)
  tint: '#f4eee2', // the box ground
  rule: '#cfc5b2', // hairlines
  muted: '#6a655c', // running heads, credits
  paper: '#ffffff',
};
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
const colorPalette = [
  ...Object.entries(palette).map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } })),
  { id: 'main-color', name: 'brass (defaults)', value: { hex: palette.brass, model: 'hex' } },
];
// #endregion
const [TEXT, LABEL] = ['Source Serif 4', 'Source Sans 3'];
const PAGE = { w: 148, h: 210 }; // mm: an A5 booklet
const LEAD = 14; // pt

// #region cover: the photograph to the trim, the title on its dark head
const at = (edge, x, y, width) => ({ anchor: { to: 'page', edge },
  offset: { x: mm(x), y: mm(y) }, ...(width ? { size: { width: mm(width) } } : {}) });
const caps = { fontFamily: LABEL, fontWeight: 700, textTransform: 'uppercase',
  letterSpacing: pt(1.6) };
// Every side is named by the direction of the text: 'top-start' is the left of an English
// page and the right of an Arabic one, so the cover needs no override to turn round.
const cover = { id: 'cover',
  advancedDesign: { enabled: true, slot: { elements: [
    { kind: 'image', id: 'photo', resourceId: 'rasulid', decorative: true, fit: 'cover',
      placement: { anchor: { to: 'page', edge: 'top-left' },
        size: { width: mm(PAGE.w), height: mm(PAGE.h) } } },
    { kind: 'text', id: 'kicker', content: '{attr.kicker}', fontSize: pt(8.5), align: 'start',
      color: col('paper'), ...caps, placement: at('top-start', 17, 18) },
    { kind: 'rule', id: 'rule', direction: 'horizontal', thickness: pt(1.5), color: col('tint'),
      placement: { anchor: { to: '#kicker', edge: 'below' }, offset: { x: mm(0), y: mm(3) },

Le script.js composé s’exécute tel quel : collez-le dans le script de module de n’importe quelle page, ou ouvrez la recette sur CodePen. Dossier de la recette sur GitHub ↗ (s’ouvre dans un nouvel onglet)

Variantes

#Écrire les surcharges dans un paquet

L’aller-retour .postext écrit un livre en deux langues dans un fichier. Depuis postext 1.58, createBundle enregistre une fois les réglages généraux et, à côté des chapitres de chaque langue, sa surcharge, telle que mergeConfigLayers la lit ici. Le livre de cette recette s’ouvre ainsi dans le Sandbox : passez le panneau de mise en page de Toutes les langues à l’édition arabe pour voir les champs que fixe l’édition arabe.

#Imprimer l’édition arabe pour le Maghreb

ar-MA garde le sens et la reliure et écrit en chiffres européens les nombres générés, folios et numéros de figure. Retapez aussi les chiffres du tableau.

-  locale: t({ en: 'en-gb', es: 'es', ar: 'ar' }), // ar: right to left, bound on the right
+  locale: t({ en: 'en-gb', es: 'es', ar: 'ar-MA' }), // ar-MA: European digits

Erreurs fréquentes

Erreur fréquente

Les polices arabes ont besoin de leur fichier arabic, avec le bloc arabic

Fontsource sert Amiri, Noto Naskh Arabic ou Scheherazade New sous la forme d’un fichier par sous-ensemble, et les lettres arabes se trouvent dans le fichier arabic. loadFonts ne récupère que latin (et latin-ext) : à l’écran, l’arabe vient d’une police système et est mal mesuré, et fontsourceProvider remet ce fichier latin au PDF, qui imprime des cases vides. Listez le bloc arabic du kit, appelez loadArabicFonts(FONTS, markdown) après loadFonts, avec dans FONTS chaque graisse que les pages composent en arabe, et donnez à renderToPdf fontProvider: arabicPdfProvider. Polices arabes →

  • Les lettres arabes se lient : un libellé interlettré dans les réglages généraux doit perdre son letterSpacing dans la surcharge arabe, sinon le moteur émet un avertissement joiningScriptLetterSpacing. textTransform: 'none' l’accompagne : l’arabe n’a pas de capitales.
  • Les lignes d’une édition arabe sont plus hautes et ses pages se coupent ailleurs. Chaque édition a été ajustée à part : l’encadré arabe flotte en pied de page (placement="bottom") et ses phrases de clôture courent à côté de la dernière photographie.

Crédits

Texte
Texte original, CC BY 4.0
Images
Polices
Source Serif 4 (SIL OFL 1.1) · Source Sans 3 (SIL OFL 1.1) · Amiri (SIL OFL 1.1) · IBM Plex Sans Arabic (SIL OFL 1.1)
SandboxPDF