Pular para o conteúdo principal
Receita número 178

Receitas · Capítulo 10 · Saída e integração

Um projeto gráfico para três edições em três idiomas

Um livreto em inglês, espanhol e árabe a partir de ajustes gerais, com substituições só para o que é de cada idioma: fontes, corpos, espacejamento.

Nesta página
Gênero
Catálogos
Saída
Canvas · PDF
Nível
Avançado
Postext
Testada com o Postext 1.65.0
Requer ≥ 1.65.0 · postext-pdf ≥ 1.65.0
Licença
Atualizada em 31 de out. de 2026
Código MIT · Texto CC BY 4.0
  • Amostra em inglês: ainda sem edição em português
  • Refile 148 × 210 mm
  • 1 coluna
  • Source Serif 4 10/14
  • Source Sans 3
  • 4 páginas
  • Nível
  • Postext 1.65.0
  • Diagramado em 223 ms
  • 205 linhas de código

Em poucas palavras

Um livreto sobre três astrolábios antigos, impresso em inglês, espanhol e árabe. Os três compartilham um projeto; só a edição árabe troca as fontes e os tamanhos.

O que você vai compor

Um livreto de quatro páginas A5 sobre três astrolábios do Metropolitan Museum of Art: um feito no Iêmen em 1291, outro no Irã em 1654 e outro na Europa em 1462. A editora o publica em inglês, espanhol e árabe, e as três edições são um só projeto. A foto de capa, os títulos cor de latão, o box com seu fio, as figuras com o texto ao lado e o cabeçalho escuro da tabela saem dos mesmos ajustes. A edição árabe troca o que é da sua escrita: Amiri e IBM Plex Sans Arabic no lugar de Source Serif e Source Sans, um corpo maior e rótulos sem maiúsculas nem espacejamento. Ela se lê a partir da direita e é encadernada à direita sem uma única coordenada própria.

Esta receita responde a

  • Como compartilho um projeto gráfico entre as edições de um livro em vários idiomas e mudo só as fontes, a hifenização, as aspas e os rótulos de cada idioma?
  • Como publico o mesmo livro em dois idiomas a partir de um só projeto?

A resposta curta

script.js · linhas 114–141no código 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

Ingredientes

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

Preparo

#1 · Ajustes gerais e três pequenas substituições

O código está na resposta curta acima. config guarda os ajustes gerais; mergeConfigLayers põe por cima a substituição de uma edição, campo a campo. Inglês e espanhol não têm substituição: o locale deles escolhe os padrões de hifenização e as palavras Figure e Table, Figura e Tabla. A substituição árabe nomeia só campos soltos: bodyText.fontFamily troca a fonte e mantém a cor, o alinhamento e a hifenização, e o elemento kicker da capa troca a fonte e o espacejamento e mantém a âncora, a cor e o tamanho. Uma mudança na paleta ou nas margens em config chega às três edições.

#2 · Lados nomeados pela direção do texto

script.js · linhas 59–72no código 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),
] };

A borda externa de uma página par é o início das suas linhas nas duas direções, e a de uma ímpar, o fim; por isso top-start e top-end levam os fólios para fora tanto numa página par inglesa quanto numa árabe. O fio do box é side: 'start' e o texto da capa se ancora em top-start: nenhum precisa de substituição.

#3 · Figuras no fim da linha, com o texto ao lado

script.js · linhas 195–219no código 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' põe cada foto à direita de uma coluna inglesa e à esquerda de uma árabe, e o texto corre ao lado. Legendas e textos alternativos vêm no idioma de cada edição; as legendas árabes dão primeiro a data da Hégira.

#4 · A tabela com as palavras e os algarismos de cada edição

script.js · linhas 223–241no código 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سم')] }) } } });

Os dados da tabela são conteúdo e mudam com a edição: algarismos arábico-índicos e o separador decimal árabe na edição árabe, vírgula decimal na espanhola. O estilo é geral: a substituição só define a fonte e o corpo árabes.

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

O script.js montado funciona como está: cole-o como script de módulo em qualquer página ou abra a receita no CodePen. Pasta da receita no GitHub ↗ (abre em uma nova aba)

Variações

#Grave as substituições num pacote

A ida e volta do .postext grava um livro em dois idiomas num arquivo. Desde o postext 1.58, createBundle salva uma vez os ajustes gerais e, junto aos capítulos de cada idioma, a sua substituição, como mergeConfigLayers a lê aqui. O livro desta receita abre assim no Sandbox: alterne o painel de design entre Todos os idiomas e a edição árabe para ver que campos a edição árabe define.

#Imprima a edição árabe para o Magrebe

ar-MA mantém a direção e a encadernação e escreve em algarismos europeus os números gerados, fólios e números das figuras. Redigite também os algarismos da tabela.

-  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

Erros comuns

Erro comum

Fontes árabes precisam do seu arquivo arabic, pelo bloco arabic

O Fontsource serve Amiri, Noto Naskh Arabic ou Scheherazade New em um arquivo por subconjunto, e as letras árabes estão no arquivo arabic. loadFonts baixa só latin (e latin-ext), então na tela o árabe vem de uma fonte do sistema e é medido errado, e o fontsourceProvider entrega ao PDF esse arquivo latin, que imprime caixas vazias. Inclua o bloco arabic do kit, chame loadArabicFonts(FONTS, markdown) depois de loadFonts, com todos os pesos que as páginas usam em árabe listados em FONTS, e passe a renderToPdf fontProvider: arabicPdfProvider. Fontes árabes →

  • As letras árabes se ligam: um rótulo espacejado nos ajustes gerais precisa perder o letterSpacing na substituição árabe, senão o motor emite o aviso joiningScriptLetterSpacing. Com ele vai textTransform: 'none': o árabe não tem maiúsculas.
  • As linhas de uma edição árabe são mais altas e as páginas quebram em outros pontos. Cada edição foi ajustada por conta própria: o box árabe flutua para o pé da página (placement="bottom") e as frases de fecho correm ao lado da última foto.

Créditos

Texto
Texto original, CC BY 4.0
Imagens
Fontes
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