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
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
- Fonctionnalités
- Surcharges par langueTexte de droite à gauchePolices arabesLivres reliés à droiteChiffres des nombres générésCésure et langue du documentFigure et Tableau dans votre langueCouvertures, pages de titre et colophonsStyles de titreAttributs de titreImages dans les maquettes de pageTitres courants et foliosHabillage du texte autour des images et des encadrésEncadrésStyle de tableauStyle des légendesPalette de couleurs sémantiqueExport PDF
- Utilise aussi
- Des figures exactement iciBandeau de chapitre sur toute la largeurOuvertures composéesTitres courants selon le rôle de la pageStyles de paragraphePolices intégrées au PDFFigures et tableaux comme ressourcesSauts de ligne dans les titres
- Typographie
- Source Serif 4, Source Sans 3, Amiri, IBM Plex Sans Arabic (SIL OFL 1.1)
- Ressources
mashhad-800.jpgrasulid-cover-1400.jpgregiomontanus-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
// 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
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
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
// ═══ 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 digitsErreurs 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
letterSpacingdans la surcharge arabe, sinon le moteur émet un avertissementjoiningScriptLetterSpacing.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
- Recette
- Ignacio Ferro
- Texte
- Texte original, CC BY 4.0
- Images
- 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
- 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)
Modifier ce texte ↗ (s’ouvre dans un nouvel onglet)Dossier de la recette sur GitHub ↗ (s’ouvre dans un nouvel onglet)


