diff --git a/CHANGELOG.md b/CHANGELOG.md index f0b52b5..a013c56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Short notes for changes on branch **`v2`** relative to **`main`** (no dedicated ### Added - **Multi-line alignment** — projects and preview support more than two lines; links only between vertically adjacent lines. +- **Vertical writing systems** — a project-wide layout direction (`layoutAxis`): `rows` as before, or `columns`, where every line becomes a vertical column and connectors run sideways. Per line, a `textOrientation` of `upright`, `vertical` (stacked characters, for Japanese and Chinese), or `sideways` (rotated line, for traditional Mongolian and for Latin runs inside vertical text). Preview, all four export formats, and share links carry both. Column side follows line order; there is no separate direction setting. Gallery examples `japanese-vertical-writing-tategaki-alignment` and `traditional-mongolian-vertical-alignment`, plus the guide `/guide/vertical-writing-alignment`. Not yet exposed through the HTTP API, the MCP server, or vertical shaping of uploaded custom fonts in PNG/PDF export. - **Line editor** — line cards (`LineCard`), modal text editing (`LineEditModal`), line settings (font, size, spacing, LTR/RTL, etc.) via popover/sheet. - **Per-pair line controls** — vertical gap slider (`LinePairGapSlider`), toggling connector visibility for each adjacent line pair (`pairControls`). - **Tokenization settings** — Tokens tab (word split characters, join character, optional punctuation tokenization); “?” hints (`SettingsFieldHint`). diff --git a/README.md b/README.md index 87a074f..649d8a0 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ No accounts, no machine translation — you stay in control of every link. Great - **Manual word linking** — click a word, then its match; supports one-to-many and many-to-one links and freely crossing connectors for reordered translations. - **Interlinear glosses & IPA** — add annotation tiers above or below any line. - **Right-to-left scripts** — Hebrew, Arabic, and mixed LTR/RTL layouts. +- **Vertical writing systems** — stand the whole diagram up as columns for Japanese tategaki, Chinese, or traditional Mongolian; characters stack or the line rotates, chosen per line, so a horizontal translation stays readable beside it. - **Tokenization control** — choose how text splits into word boxes (split characters, a join marker for fixed expressions, optional punctuation handling). - **Per-line typography** — font, size, spacing, and direction per line; custom and Google Fonts. - **Exports** — PNG, SVG, PDF, and a self-contained HTML file; exports match the preview, including custom-font shaping. @@ -28,7 +29,7 @@ No accounts, no machine translation — you stay in control of every link. Great ## Examples -Browse ready-made examples — bilingual pairs, Turkish interlinear with IPA, RTL scripts, Tagalog compounds, Japanese–Chinese–English word order, and more interlinear glosses — at **[aligner.tinygods.dev/examples](https://aligner.tinygods.dev/examples)**. Open any one in the editor to adapt it. +Browse ready-made examples — bilingual pairs, Turkish interlinear with IPA, RTL scripts, vertical Japanese and traditional Mongolian, Tagalog compounds, Japanese–Chinese–English word order, and more interlinear glosses — at **[aligner.tinygods.dev/examples](https://aligner.tinygods.dev/examples)**. Open any one in the editor to adapt it. ![Turkish interlinear preview with glosses, IPA, and connectors](bitext/static/screenshots/preview-turkish-example.png) diff --git a/bitext/.env.example b/bitext/.env.example index f8e2f9c..27274b9 100644 --- a/bitext/.env.example +++ b/bitext/.env.example @@ -22,3 +22,5 @@ OBJECT_STORAGE_CDN_ENDPOINT= # Render script (optional) # PREVIEW_URL=http://127.0.0.1:4173 # PREVIEW_PORT=4173 +# Use a system Chromium instead of Playwright's download (needed on NixOS/Guix) +# CHROMIUM_PATH=/usr/bin/chromium diff --git a/bitext/package-lock.json b/bitext/package-lock.json index 7e54fa0..2b80a1a 100644 --- a/bitext/package-lock.json +++ b/bitext/package-lock.json @@ -3625,9 +3625,9 @@ "license": "MIT" }, "node_modules/dompurify": { - "version": "3.4.12", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.12.tgz", - "integrity": "sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==", + "version": "3.4.13", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.13.tgz", + "integrity": "sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==", "license": "(MPL-2.0 OR Apache-2.0)", "optional": true, "optionalDependencies": { @@ -4934,9 +4934,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "dev": true, "funding": [ { diff --git a/bitext/scripts/README.md b/bitext/scripts/README.md index 72317da..cc8f2e8 100644 --- a/bitext/scripts/README.md +++ b/bitext/scripts/README.md @@ -10,6 +10,14 @@ npm install npx playwright install chromium ``` +Playwright's bundled Chromium is dynamically linked and will not start on distros that do not +provide its libraries (NixOS, Guix report `libglib-2.0.so.0: cannot open shared object file`). +Point the render script at a system browser instead: + +```bash +CHROMIUM_PATH=$(which chromium) npm run examples:render +``` + Copy `.env.example` to `.env` in the `bitext/` folder and fill in object storage credentials. ## Render + upload diff --git a/bitext/scripts/render-example-previews.ts b/bitext/scripts/render-example-previews.ts index fac90af..a556cae 100644 --- a/bitext/scripts/render-example-previews.ts +++ b/bitext/scripts/render-example-previews.ts @@ -63,6 +63,9 @@ function resolveRenderFonts(): { css: string; skip: Set } { return { css: faces.join('\n'), skip }; } +/** Path to a system Chromium; unset means Playwright's own download. */ +const CHROMIUM_EXECUTABLE = process.env.CHROMIUM_PATH?.trim() || undefined; + const PREVIEW_PORT = Number(process.env.PREVIEW_PORT ?? 4173); const PREVIEW_URL = (process.env.PREVIEW_URL ?? `http://127.0.0.1:${PREVIEW_PORT}`).replace( /\/$/, @@ -228,7 +231,11 @@ async function main(): Promise { const { css: renderFontCss, skip: skipSlugs } = resolveRenderFonts(); const renderedSlugs: string[] = []; - const browser = await chromium.launch(); + // Playwright's bundled Chromium is dynamically linked against libs some distros (NixOS, + // Guix) do not provide, so allow pointing at a system browser instead. + const browser = await chromium.launch( + CHROMIUM_EXECUTABLE ? { executablePath: CHROMIUM_EXECUTABLE } : {} + ); try { const page = await browser.newPage({ viewport: { width: EXAMPLE_RENDER_WIDTH + 80, height: 1200 }, diff --git a/bitext/src/app.css b/bitext/src/app.css index d3aa592..0bf5577 100644 --- a/bitext/src/app.css +++ b/bitext/src/app.css @@ -244,6 +244,45 @@ html { align-items: stretch; } +/* Column mode: lines sit side by side, first line leftmost — the same "first is at the start of + the stack" rule as rows. Which script ends up on which side is set by line order. */ +/* Columns stretch to the tallest one rather than to a percentage height, so the same rules work + whether the frame is bounded (editor) or hugs its content (readonly embed, headless render). */ +.preview-stack--columns { + flex-direction: row; + flex-wrap: wrap; + align-items: stretch; + justify-content: center; +} + +/* Column mode grows sideways, so the block axis needs a bound for auto-fit to shrink a long + sentence into. Only while editing: a readonly embed has no viewport to fit, and forcing one + there would screenshot mostly blank canvas. */ +.preview-frame[data-aligner-axis='columns']:not([data-aligner-readonly]) { + height: min(70vh, 44rem); +} + +.preview-frame[data-aligner-axis='columns']:not([data-aligner-readonly]) .preview-zoom, +.preview-frame[data-aligner-axis='columns']:not([data-aligner-readonly]) .preview-stack--columns { + height: 100%; +} + +.preview-stack--columns .preview-token-line { + min-height: 0; +} + +.preview-stack--columns .preview-gloss-wrap { + width: auto; + min-height: 0; + align-items: center; +} + +/* The credit is a flex item of the stack, so in a row it would sit beside the diagram. Give it a + full basis and it wraps onto its own line underneath. */ +.preview-stack--columns .preview-frame__attribution { + flex-basis: 100%; +} + .preview-frame__attribution { position: relative; z-index: 3; @@ -378,6 +417,29 @@ path.link-path { flex-wrap: wrap; } +/* Column mode: setting the row itself vertically makes flex direction and the logical + `margin-inline-start` between tokens map to the block axis with no extra branching. + Baseline alignment is dropped because a column may mix upright and vertically set tokens. */ +.token-row--columns { + writing-mode: vertical-rl; + align-items: center; +} + +/* Glyph setting per line. `upright` is explicit so it resets the inherited vertical row above. */ +.token-view--upright { + writing-mode: horizontal-tb; +} + +.token-view--vertical { + writing-mode: vertical-rl; + text-orientation: upright; +} + +.token-view--sideways { + writing-mode: vertical-rl; + text-orientation: sideways; +} + .token-view { display: inline-flex; align-items: baseline; diff --git a/bitext/src/lib/components/editor/LineSettingsForm.svelte b/bitext/src/lib/components/editor/LineSettingsForm.svelte index 016a079..4285452 100644 --- a/bitext/src/lib/components/editor/LineSettingsForm.svelte +++ b/bitext/src/lib/components/editor/LineSettingsForm.svelte @@ -3,6 +3,7 @@ import { TrashBinOutline } from 'flowbite-svelte-icons'; import { Label, Range } from 'flowbite-svelte'; import type { LineV2 } from '$lib/serialization/schema.js'; + import type { TextOrientation } from '$lib/types/layout.js'; import { GOOGLE_FONT_OPTIONS } from '$lib/fonts/google-fonts.js'; import { MAX_TEXT_SIZE_PX, @@ -258,6 +259,21 @@ /> Right-to-left row +
+ + +
{#if nextLine}