Files
KantSites/tests/translate.test.mjs
kantarciandClaude Opus 5.5 e15b2517b1 Translate pages, areas, menus, forms and posts field by field instead of rebuilding them
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>
2026-10-09 16:56:52 +02:00

56 lines
3.3 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
});