import { describe, it, expect } from 'vitest'; import { parseTranslation, processDocument, processPage, segmentId, validateTranslationPair, } from '../../scripts/content-segments.mjs'; interface Collected { id: string; markup: string; } function parse(body: string, head = ''): Document { return new DOMParser().parseFromString( `${head}${body}`, 'text/html' ); } function collect(document: Document): Collected[] { const found: Collected[] = []; processDocument(document, { record: (id: string, markup: string): void => { found.push({ id, markup }); }, }); return found; } function translateWith( document: Document, dictionary: Record ): { translated: number; invalid: string[] } { const ids = new Map(); collect(parse(document.body.innerHTML, document.head.innerHTML)).forEach( (entry) => ids.set(entry.markup, entry.id) ); const byId: Record = {}; for (const [markup, translation] of Object.entries(dictionary)) { const id = ids.get(markup) ?? segmentId(markup); byId[id] = translation; } return processDocument(document, { translate: (id: string) => byId[id] ?? null, }); } describe('content segments', () => { it('turns inline elements into numbered placeholders', () => { const doc = parse('

Read the docs now.

'); expect(collect(doc).map((entry) => entry.markup)).toEqual([ 'Read <1>the docs now.', ]); }); it('keeps links intact and allows word order to change', () => { const doc = parse( '

Read the docs now.

' ); const result = translateWith(doc, { 'Read <1>the docs now.': 'Jetzt <1>die Doku lesen.', }); expect(result.translated).toBe(1); expect(doc.body.innerHTML).toBe( '

Jetzt die Doku lesen.

' ); }); it('treats code and icons as untouched placeholders', () => { const doc = parse( '

Use qpdf --check first

' ); expect(collect(doc).map((entry) => entry.markup)).toEqual([ 'Use <1/> first <2/>', ]); translateWith(doc, { 'Use <1/> first <2/>': 'Zuerst <1/> nutzen <2/>' }); expect(doc.body.innerHTML).toBe( '

Zuerst qpdf --check nutzen

' ); }); it('rejects a translation that loses a placeholder and keeps English', () => { const doc = parse('

Read the docs now.

'); const result = translateWith(doc, { 'Read <1>the docs now.': 'Jetzt die Doku lesen.', }); expect(result.translated).toBe(0); expect(result.invalid).toHaveLength(1); expect(doc.body.innerHTML).toBe( '

Read the docs now.

' ); }); it('rejects unbalanced or foreign tags', () => { const doc = parse('

One two three

'); const elements = { 1: doc.querySelector('b') as Element, }; expect(parseTranslation('Eins <1>zwei drei', elements)).not.toBeNull(); expect(parseTranslation('Eins <1>zwei drei', elements)).toBeNull(); expect(parseTranslation('Eins <2>zwei drei', elements)).toBeNull(); expect(parseTranslation('Eins ' ); translateWith(doc, { 'Is it free?<1/>': 'Ist es kostenlos?<1/>', 'Yes, always.': 'Ja, immer.', }); const data = JSON.parse( doc.querySelector('script[type="application/ld+json"]')?.textContent ?? '{}' ); expect(data.mainEntity[0].name).toBe('Ist es kostenlos?'); expect(data.mainEntity[0].acceptedAnswer.text).toBe('Ja, immer.'); }); it('validates source and translation pairs', () => { expect(validateTranslationPair('A <1>b', 'X <1>y')).toBeNull(); expect(validateTranslationPair('A <1>b', 'X y')).toBe( 'placeholders differ' ); expect(validateTranslationPair('A', '')).toBe('empty'); expect(validateTranslationPair('A', 'B c')).toBe( 'contains raw angle brackets' ); }); it('keeps segment ids stable so stored translations stay valid', () => { expect(segmentId('How to deskew PDF online')).toBe('001b11bf2ae1'); expect(segmentId('Merge PDF')).not.toBe(segmentId('Split PDF')); }); it('rejects translations that nest markers differently or use unknown entities', () => { expect( validateTranslationPair('A <1>b <2>c', 'X <1>y <2>z') ).toBe('placeholders nested differently'); expect(validateTranslationPair('A <1>b', 'X <1>y<2>')).toBe( 'placeholders differ' ); expect(validateTranslationPair('A <1>b', 'X <1>y…')).toBe( 'unsupported entity …' ); expect(validateTranslationPair('A & B', 'X & Y')).toBeNull(); expect(validateTranslationPair('A <1/> b', '<1/> x')).toBeNull(); }); it('keeps translated markup-like text as plain text', () => { const doc = parse('

Hello world

'); translateWith(doc, { 'Hello <1>world': 'Hallo <img onerror=alert(1)> <1>Welt', }); expect(doc.querySelector('img')).toBeNull(); expect(doc.querySelector('p')?.textContent).toBe( 'Hallo Welt' ); }); it('uses the whole answer for FAQ structured data, not just the first paragraph', () => { const doc = parse( '
Why?

Two reasons:

  • One
  • Two
' ); translateWith(doc, { 'Why?': 'Warum?', 'Two reasons:': 'Zwei Gründe:', One: 'Eins', Two: 'Zwei', }); const data = JSON.parse( doc.querySelector('script[type="application/ld+json"]')?.textContent ?? '{}' ); expect(data.mainEntity[0].acceptedAnswer.text).toBe( 'Zwei Gründe: Eins Zwei' ); }); it('skips elements marked data-no-i18n', () => { const doc = parse( '

Hello

' ); expect(collect(doc).map((entry) => entry.markup)).toEqual(['Hello']); }); });