Vai al contenuto principale
Ricetta numero 178

Ricettario · Capitolo 10 · Output e integrazione

Un progetto grafico per tre edizioni in tre lingue

Un libretto in inglese, spagnolo e arabo da impostazioni generali, con impostazioni per lingua solo per ciò che le appartiene: caratteri, corpi, spaziatura.

In questa pagina
Genere
Cataloghi
Output
Canvas · PDF
Livello
Avanzato
Postext
Provata con Postext 1.65.0
Richiede ≥ 1.65.0 · postext-pdf ≥ 1.65.0
Licenza
Aggiornata il 31 ott 2026
Codice MIT · Testo CC BY 4.0
  • Esempio in inglese: non c’è ancora un’edizione italiana
  • Formato 148 × 210 mm
  • 1 colonna
  • Source Serif 4 10/14
  • Source Sans 3
  • 4 pagine
  • Livello
  • Postext 1.65.0
  • Impaginato in 223 ms
  • 205 righe di codice

In breve

Un libretto su tre antichi astrolabi, stampato in inglese, spagnolo e arabo. I tre condividono un progetto; solo l’edizione araba cambia caratteri e corpi.

Che cosa comporrai

Un libretto di quattro pagine A5 su tre astrolabi del Metropolitan Museum of Art: uno fatto nello Yemen nel 1291, uno in Iran nel 1654 e uno in Europa nel 1462. L’editore lo pubblica in inglese, spagnolo e arabo, e le tre edizioni sono un solo progetto. La foto di copertina, i titoli color ottone, il riquadro con il suo filetto, le figure con il testo accanto e l’intestazione scura della tabella vengono dalle stesse impostazioni. L’edizione araba cambia ciò che appartiene alla sua scrittura: Amiri e IBM Plex Sans Arabic al posto di Source Serif e Source Sans, un corpo più grande ed etichette senza maiuscole né spaziatura. Si legge da destra e si rilega a destra senza una sola coordinata propria.

Questa ricetta risponde a

  • Come condivido un progetto grafico tra le edizioni di un libro in più lingue e cambio solo caratteri, sillabazione, virgolette ed etichette di ciascuna lingua?
  • Come pubblico lo stesso libro in due lingue da un unico progetto?

La risposta breve

script.js · righe 114–141nel codice completo
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

Ingredienti

Tipo
Source Serif 4, Source Sans 3, Amiri, IBM Plex Sans Arabic (SIL OFL 1.1)
Risorse
  • 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)

Preparazione

#1 · Impostazioni generali e tre piccole impostazioni per lingua

Il codice è nella risposta breve qui sopra. config contiene le impostazioni generali; mergeConfigLayers vi sovrappone quelle di un’edizione campo per campo. Inglese e spagnolo non ne hanno di proprie: il loro locale sceglie i modelli di sillabazione e le parole Figure e Table, Figura e Tabla. Le impostazioni arabe nominano solo campi singoli: bodyText.fontFamily cambia il carattere e conserva colore, allineamento e sillabazione, e l’elemento kicker della copertina cambia carattere e spaziatura e conserva ancora, colore e dimensione. Un cambio della tavolozza o dei margini in config arriva a tutte e tre le edizioni.

#2 · Lati nominati secondo la direzione del testo

script.js · righe 59–72nel codice completo
// 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),
] };

Il margine esterno di una pagina pari è l’inizio delle sue righe in entrambe le direzioni, quello di una dispari la loro fine; così top-start e top-end portano i folii all’esterno su una pagina pari inglese come su una araba. Il filetto del riquadro è side: 'start' e il testo di copertina si ancora a top-start: nessuno ha bisogno di impostazioni proprie.

#3 · Figure a fine riga, con il testo accanto

script.js · righe 195–219nel codice completo
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' mette ogni fotografia a destra di una colonna inglese e a sinistra di una araba, e il testo scorre accanto. Didascalie e testi alternativi sono nella lingua di ogni edizione; le didascalie arabe danno prima la data dell’egira.

#4 · La tabella con le parole e le cifre di ogni edizione

script.js · righe 223–241nel codice completo
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سم')] }) } } });

I dati della tabella sono contenuto e cambiano con l’edizione: cifre arabo-indiane e separatore decimale arabo nell’edizione araba, virgola decimale in quella spagnola. Il suo stile è generale: le impostazioni arabe fissano solo carattere e corpo.

La ricetta completa

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) },

Lo script.js composto funziona così com’è: incollalo nello script modulo di qualsiasi pagina, oppure apri la ricetta su CodePen. Cartella della ricetta su GitHub ↗ (si apre in una nuova scheda)

Varianti

#Scrivi le impostazioni per lingua in un pacchetto

Andata e ritorno del .postext scrive un libro in due lingue in un file. Da postext 1.58 createBundle salva una volta le impostazioni generali e, accanto ai capitoli di ogni lingua, le sue impostazioni proprie, come le legge qui mergeConfigLayers. Il libro di questa ricetta si apre così nel Sandbox: passa il pannello di design da Tutte le lingue all’edizione araba per vedere quali campi fissa l’edizione araba.

#Stampa l’edizione araba per il Maghreb

ar-MA conserva direzione e rilegatura e scrive in cifre europee i numeri generati, folii e numeri delle figure. Riscrivi anche le cifre della tabella.

-  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

Errori frequenti

Errore frequente

I caratteri arabi hanno bisogno del loro file arabic, tramite il blocco arabic

Fontsource serve Amiri, Noto Naskh Arabic o Scheherazade New come un file per sottoinsieme, e le lettere arabe stanno nel file arabic. loadFonts scarica solo latin (e latin-ext), così a schermo l’arabo viene da un font di sistema ed è misurato male, e fontsourceProvider passa al PDF quel file latin, che stampa riquadri vuoti. Elenca il blocco arabic del kit, chiama loadArabicFonts(FONTS, markdown) dopo loadFonts, con ogni peso che le pagine compongono in arabo elencato in FONTS, e passa a renderToPdf fontProvider: arabicPdfProvider. Font arabi →

  • Le lettere arabe sono legate: un’etichetta spaziata nelle impostazioni generali deve perdere il suo letterSpacing in quelle arabe, altrimenti il motore dà l’avviso joiningScriptLetterSpacing. Con esso va textTransform: 'none': l’arabo non ha maiuscole.
  • Le righe di un’edizione araba sono più alte e le sue pagine si spezzano altrove. Ogni edizione è stata adattata a parte: il riquadro arabo galleggia al piede della pagina (placement="bottom") e le frasi di chiusura scorrono accanto all’ultima fotografia.

Crediti

Testo
Testo originale, CC BY 4.0
Immagini
Font
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