A site can have further languages (Settings → Languages). A page is built once; each language adds only its texts, keyed by the stable block IDs, plus its own address under /en/… (ADR 111, document schema v27, migration 20261009000100 for page_translation). - Editor: DE | EN switch, a list of all texts with original, state (missing, translated, original changed) and filter; structure stays with the main language. Undo, drafts, publishing and revisions cover all languages at once. - Website: /en and translated slugs, lang, canonical, hreflang with x-default, og:locale, sitemap; links to translated pages follow the language; a language switch block. - Header, footer and linked sections translate the same way; menu entries carry labels per language; fixed template texts come from a small dictionary. - Forms: labels, help, options, button and success message per language; messages and the step script speak the language, the submission and the email stay in the main language. - Posts, archives, category names, consent text and the 404 page follow the language. - Single-language sites see none of it and never touch the new table. The forms administration becomes an overview with one row per form and its own edit page; fields are one line each with details collapsed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
56 lines
3.3 KiB
JavaScript
56 lines
3.3 KiB
JavaScript
import test from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import { readFileSync } from 'node:fs';
|
||
import { fingerprint, entries, progress, setText, confirmText, setPageValue, FIELDS } from '../public/assets/translate.js';
|
||
import { EditorHistory } from '../public/assets/editor-model.js';
|
||
|
||
const page = () => ({
|
||
schemaVersion: 26, title: 'Leistungen', description: 'Was wir tun',
|
||
blocks: [{ id: 's1', type: 'section', children: [
|
||
{ id: 'h1', type: 'heading', props: { text: 'Leistungen', level: 1 } },
|
||
{ id: 'b1', type: 'button', props: { text: 'Kontakt', href: '/kontakt' } },
|
||
{ id: 'i1', type: 'image', props: { src: '/x.svg', alt: '', decorative: true, width: 1, height: 1 } },
|
||
{ id: 'q1', type: 'quote', props: { text: 'Sehr gut', author: 'Anna', role: '', rating: 5 } },
|
||
] }],
|
||
});
|
||
|
||
test('the fingerprint matches hash("fnv1a32") in PHP, also for umlauts and markup', () => {
|
||
// Werte aus PHP 8.4: hash('fnv1a32', …)
|
||
assert.equal(fingerprint('Leistungen'), '16a48bdd');
|
||
assert.equal(fingerprint('Über uns – <b>ß</b>'), '80c36c2f');
|
||
});
|
||
|
||
test('the translatable fields mirror Translations::FIELDS on the server', () => {
|
||
const php = readFileSync(new URL('../src/Content/Translations.php', import.meta.url), 'utf8');
|
||
const server = Object.fromEntries([...php.matchAll(/'([a-z]+)' => \[((?:'[a-zA-Z]+'(?:, )?)+)\],/g)].map(([, type, list]) => [type, [...list.matchAll(/'([a-zA-Z]+)'/g)].map(match => match[1])]));
|
||
assert.deepEqual(Object.fromEntries(Object.entries(FIELDS).map(([type, fields]) => [type, fields.map(field => field.prop)])), server);
|
||
});
|
||
|
||
test('translating writes only into translations, notes the original and can be undone', () => {
|
||
const original = page();
|
||
const history = new EditorHistory(original);
|
||
assert.deepEqual(progress(history.document, 'en'), { total: 6, done: 0, stale: 0 }, 'Leere und dekorative Felder zählen nicht.');
|
||
|
||
history.change(doc => setText(doc, 'en', 'h1.text', 'Services'));
|
||
history.change(doc => setText(doc, 'en', 'title', 'Services'));
|
||
history.change(doc => setPageValue(doc, 'en', 'slug', ' Services '));
|
||
const doc = history.document;
|
||
assert.equal(doc.schemaVersion, 27);
|
||
assert.equal(doc.blocks[0].children[0].props.text, 'Leistungen', 'Die Hauptsprache bleibt unberührt.');
|
||
assert.deepEqual(doc.translations.en, { blocks: { h1: { text: 'Services' } }, source: { 'h1.text': '16a48bdd', title: '16a48bdd' }, title: 'Services', slug: 'services' });
|
||
assert.deepEqual(progress(doc, 'en'), { total: 6, done: 2, stale: 0 });
|
||
|
||
// Das Original ändert sich: die Übersetzung gilt als veraltet, bis sie bestätigt wird.
|
||
history.change(content => { content.blocks[0].children[0].props.text = 'Unsere Leistungen'; });
|
||
assert.equal(entries(history.document, 'en').find(entry => entry.key === 'h1.text').state, 'stale');
|
||
history.change(content => confirmText(content, 'en', 'h1.text'));
|
||
assert.equal(entries(history.document, 'en').find(entry => entry.key === 'h1.text').state, 'done');
|
||
|
||
// Leeren entfernt den Text samt Vermerk; eine leere Sprache verschwindet ganz.
|
||
history.change(content => { setText(content, 'en', 'h1.text', ''); setText(content, 'en', 'title', ''); setPageValue(content, 'en', 'slug', ''); });
|
||
assert.equal(history.document.translations, undefined);
|
||
|
||
while (history.past.length) history.undo();
|
||
assert.deepEqual(history.document, original);
|
||
});
|