path = tempnam(sys_get_temp_dir(), 'kant-lang-'); $this->db = ConnectionFactory::create('sqlite:///'.str_replace('\\', '/', $this->path)); TestSchema::build($this->db); $this->db->insert('website', ['id' => 'default', 'name' => 'KantSites', 'default_locale' => 'de', 'theme' => '{}']); $this->settings = new SiteSettings($this->db, new MediaLibrary($this->db, $this->path.'-media')); $this->pages = new PageRepository($this->db, new DocumentValidator(), $this->settings); $this->languages = new Languages($this->db, $this->settings); $partials = new PartialRepository($this->db, new DocumentValidator()); foreach (DemoContent::areas() as $kind => $area) { $partials->create($kind, 'default', $area['name'], $area['document']); } } protected function tearDown(): void { $this->db->close(); gc_collect_cycles(); @unlink($this->path); } private function document(string $title = 'Leistungen', array $translations = []): array { $document = [ 'schemaVersion' => 27, 'title' => $title, 'description' => 'Was wir für Sie tun', 'blocks' => [[ 'id' => 's1', 'type' => 'section', 'children' => [ ['id' => 'h1', 'type' => 'heading', 'props' => ['text' => $title, 'level' => 1]], ['id' => 't1', 'type' => 'text', 'props' => ['text' => '
Schreiben Sie uns über Kontakt.
']], ['id' => 'b1', 'type' => 'button', 'props' => ['text' => 'Zum Kontakt', 'href' => '/kontakt']], ['id' => 'i1', 'type' => 'image', 'props' => ['src' => '/assets/image-placeholder.svg', 'alt' => '', 'decorative' => true, 'width' => 10, 'height' => 10]], ['id' => 'l1', 'type' => 'language', 'props' => ['display' => 'code']], ], ]], ]; if ([] !== $translations) { $document['translations'] = $translations; } return $document; } /** Legt eine Seite an, speichert einen Entwurf und veröffentlicht ihn. */ private function publishedPage(string $slug, array $document, ?string $parent = null): string { $id = $this->pages->createPage($slug, $parent, 'page', $this->document()); $revision = $this->pages->saveDraft($id, $document, 1); $this->pages->publish($id, $revision, 2); return $id; } private function republish(string $id, array $document): void { $version = (int) $this->db->fetchOne('SELECT version FROM page WHERE id = ?', [$id]); $revision = $this->pages->saveDraft($id, $document, $version); $this->pages->publish($id, $revision, $version + 1); } public function testApplyLaysTranslatedTextsOverTheSamePageAndKeepsEverythingElse(): void { $document = $this->document('Leistungen', ['en' => [ 'title' => 'Services', 'blocks' => ['h1' => ['text' => 'Services'], 'b1' => ['text' => ' '], 'i1' => ['alt' => 'A picture']], ]]); $english = Translations::apply($document, 'en'); self::assertArrayNotHasKey('translations', $english); self::assertSame('Services', $english['title']); self::assertSame('Was wir für Sie tun', $english['description'], 'Ohne Übersetzung bleibt der Text der Hauptsprache.'); $children = $english['blocks'][0]['children']; self::assertSame('Services', $children[0]['props']['text']); self::assertSame(1, $children[0]['props']['level'], 'Aufbau und Einstellungen bleiben unverändert.'); self::assertSame('Zum Kontakt', $children[2]['props']['text'], 'Ein leerer Text gilt als nicht übersetzt.'); self::assertSame('/kontakt', $children[2]['props']['href']); self::assertSame('', $children[3]['props']['alt'], 'Ein dekoratives Bild bekommt auch übersetzt keinen Text.'); self::assertSame(['total' => 5, 'done' => 2, 'stale' => 0], Translations::progress($document, 'en')); } public function testTranslationsPassTheSameChecksAsTheMainLanguage(): void { $id = $this->pages->createPage('leistungen', null, 'page', $this->document()); // Formatierter Text geht durch denselben Filter, Einträge verschwundener Blöcke fallen weg. $this->pages->saveDraft($id, $this->document('Leistungen', ['en' => [ 'blocks' => [ 'h1' => ['text' => 'Services', 'level' => 'kein Text-Feld'], 'weg' => ['text' => 'Block gibt es nicht'], ], 'source' => ['h1.text' => Translations::fingerprint('Leistungen'), 'weg.text' => '00000000'], ]]), 1); $stored = $this->pages->findForEditor($id)['document']['translations']['en']; self::assertSame(['h1' => ['text' => 'Services']], $stored['blocks']); self::assertSame(['h1.text' => Translations::fingerprint('Leistungen')], $stored['source']); $refused = [ 'unbekannte Sprache' => ['xx' => ['title' => 'X']], 'zu langer Titel' => ['en' => ['title' => str_repeat('a', 161)]], 'falsche Adresse' => ['en' => ['title' => 'Services', 'slug' => 'Unsere Leistungen']], 'unbekanntes Feld' => ['en' => ['title' => 'Services', 'menu' => 'x']], 'zu langer Button' => ['en' => ['blocks' => ['b1' => ['text' => str_repeat('a', 301)]]]], ]; foreach ($refused as $case => $translations) { try { $this->pages->saveDraft($id, $this->document('Leistungen', $translations), 2); self::fail('Abgelehnt werden muss: '.$case); } catch (InvalidDocument) { self::assertSame(2, (int) $this->db->fetchOne('SELECT version FROM page WHERE id = ?', [$id]), $case); } } $older = $this->document('Leistungen', ['en' => ['title' => 'Services']]); $older['schemaVersion'] = 26; $older['blocks'][0]['children'] = array_slice($older['blocks'][0]['children'], 0, 4); $this->expectException(InvalidDocument::class); (new DocumentValidator())->validate($older); } public function testPublishingWritesTheTranslatedAddressesAndMovesFollow(): void { $this->languages->save(['en']); $parent = $this->pages->createPage('leistungen', null, 'page', $this->document()); $revision = $this->pages->saveDraft($parent, $this->document('Leistungen', ['en' => ['title' => 'Services', 'slug' => 'services']]), 1); self::assertSame([], $this->pages->translatedPaths($parent), 'Ein Entwurf ist noch nicht unter /en erreichbar.'); $this->pages->publish($parent, $revision, 2); self::assertSame(['en' => 'services'], $this->pages->translatedPaths($parent)); $child = $this->publishedPage('webdesign', $this->document('Webdesign', ['en' => ['title' => 'Web design', 'slug' => 'web-design']]), $parent); self::assertSame(['en' => 'services/web-design'], $this->pages->translatedPaths($child)); $english = $this->pages->findPublishedTranslation('services/web-design', 'en'); self::assertSame('Web design', $english['document']['title']); self::assertSame('en', $english['locale']); self::assertSame('leistungen/webdesign', $english['path'], 'Navigation und Brotkrumen hängen an der Adresse der Hauptsprache.'); self::assertArrayNotHasKey('translations', $this->pages->findPublished('leistungen/webdesign')['document']); // Neue übersetzte Adresse der Elternseite: die Unterseite wandert mit, die alte Adresse leitet weiter. $this->republish($parent, $this->document('Leistungen', ['en' => ['title' => 'Services', 'slug' => 'offers']])); self::assertSame(['en' => 'offers/web-design'], $this->pages->translatedPaths($child)); self::assertSame(['to' => 'en/offers/web-design', 'status' => 301], (new RedirectRepository($this->db))->find('en/services/web-design')); // Verschieben in der Hauptsprache trägt die Übersetzungen mit. $other = $this->publishedPage('ueber-uns', $this->document('Über uns')); $version = (int) $this->db->fetchOne('SELECT version FROM page WHERE id = ?', [$child]); $this->pages->movePage($child, 'webdesign', $other, $version, new RedirectRepository($this->db)); self::assertSame(['en' => 'ueber-uns/web-design'], $this->pages->translatedPaths($child), 'Eine nicht übersetzte Elternseite steht mit ihrer Adresse der Hauptsprache davor.'); // Ohne Übersetzung veröffentlicht: die Sprache ist für diese Seite wieder weg. $this->republish($child, $this->document('Webdesign')); self::assertSame([], $this->pages->translatedPaths($child)); self::assertNull($this->pages->findPublishedTranslation('ueber-uns/web-design', 'en')); } public function testATranslatedAddressThatIsTakenIsRefusedWhenSaving(): void { $this->languages->save(['en']); $this->publishedPage('leistungen', $this->document('Leistungen', ['en' => ['title' => 'Services', 'slug' => 'services']])); $other = $this->pages->createPage('angebote', null, 'page', $this->document('Angebote')); $this->expectException(InvalidPage::class); $this->expectExceptionMessage('/en/services'); $this->pages->saveDraft($other, $this->document('Angebote', ['en' => ['title' => 'Offers', 'slug' => 'services']]), 1); } public function testALanguageCodeCannotCollideWithARootPage(): void { $this->pages->createPage('fr', null, 'page', $this->document('Frankreich')); try { $this->languages->save(['fr']); self::fail('Die Seite /fr würde unerreichbar.'); } catch (InvalidPage $error) { self::assertStringContainsString('/fr', $error->getMessage()); } $this->languages->save(['en', 'de', 'en']); self::assertSame(['en'], $this->languages->extra(), 'Die Hauptsprache und Doppelte fallen weg.'); $this->expectException(InvalidPage::class); $this->pages->createPage('en', null, 'page', $this->document('Englisch')); } public function testTheWebsiteServesTheTranslationWithHreflangAndLinksInItsLanguage(): void { $this->languages->save(['en']); $home = $this->publishedPage('home', $this->document('Start', ['en' => ['title' => 'Home', 'blocks' => ['h1' => ['text' => 'Welcome']]]])); $this->publishedPage('kontakt', $this->document('Kontakt', ['en' => ['title' => 'Contact', 'slug' => 'contact']])); $this->publishedPage('leistungen', $this->document('Leistungen', ['en' => [ 'title' => 'Services', 'slug' => 'services', 'seoTitle' => 'Our services | KantSites', 'blocks' => ['h1' => ['text' => 'Services'], 't1' => ['text' => 'Write to us via contact.
'], 'b1' => ['text' => 'Get in touch']], ]])); $this->publishedPage('impressum', $this->document('Impressum')); $english = $this->request('/en/services'); $html = (string) $english->getContent(); self::assertSame(200, $english->getStatusCode()); self::assertStringContainsString('Our services | KantSites', $html); self::assertStringContainsString('', $html); self::assertStringContainsString('', $html); self::assertStringContainsString('', $html); self::assertStringContainsString('hreflang="x-default" href="https://cms.test/leistungen"', $html); self::assertStringContainsString('content="en_US"', $html); self::assertStringContainsString('Get in touch', $html); self::assertStringContainsString('href="/en/contact"', $html, 'Links auf übersetzte Seiten führen zur Übersetzung.'); self::assertStringNotContainsString('href="/kontakt"', $html); self::assertMatchesRegularExpression('#DE#', $html, 'Die Sprachwahl zeigt bewusst auf die andere Sprache.'); self::assertMatchesRegularExpression('#EN#', $html); $german = (string) $this->request('/leistungen')->getContent(); self::assertStringContainsString('request('/impressum')->getContent(), 'Eine Seite ohne Übersetzung hat keine Sprachalternativen.'); self::assertStringContainsString('Welcome', (string) $this->request('/en')->getContent(), '/en ist die Startseite der Sprache.'); self::assertSame(301, $this->request('/en/home')->getStatusCode()); self::assertSame(404, $this->request('/en/impressum')->getStatusCode()); self::assertSame(404, $this->request('/fr/services')->getStatusCode()); $sitemap = (string) $this->request('/sitemap.xml')->getContent(); self::assertStringContainsString('Willkommen
']], ['id' => 'kopf-menu', 'type' => 'menu', 'props' => ['slot' => 'header', 'layout' => 'horizontal']], ]]], 'translations' => ['en' => ['blocks' => ['kopf-text' => ['text' => 'Welcome
'], 'weg' => ['text' => 'x']]]], ], $header['version']); self::assertSame(['kopf-text' => ['text' => 'Welcome
']], $partials->find($header['id'])['document']['translations']['en']['blocks'], 'Ein Bereich wird beim Speichern bereinigt wie eine Seite.'); $this->publishedPage('home', $this->document('Start')); $services = $this->publishedPage('leistungen', $this->document('Leistungen', ['en' => ['title' => 'Services', 'slug' => 'services']])); $menus = new \App\Content\MenuRepository($this->db, $this->settings); $menus->save('header', 'Hauptnavigation', [ ['label' => 'Leistungen', 'type' => 'page', 'pageId' => $services, 'anchor' => '', 'labels' => ['en' => 'Our services'], 'children' => []], ], 0); try { $menus->save('header', 'Hauptnavigation', [['label' => 'X', 'type' => 'url', 'url' => '/x', 'labels' => ['xx' => 'Y'], 'children' => []]], 1); self::fail('Eine unbekannte Sprache im Menü wird abgelehnt.'); } catch (\App\Domain\InvalidMenu) { } $english = (string) $this->request('/en/services')->getContent(); self::assertStringContainsString('Welcome
', $english); self::assertStringNotContainsString('Willkommen', $english); self::assertMatchesRegularExpression('#]*>.*?Our services#s', $english, 'Menübeschriftung und Ziel in der Sprache der Seite.'); self::assertStringContainsString('>Skip to content', $english); self::assertStringContainsString('aria-label="Main navigation"', $english); $german = (string) $this->request('/leistungen')->getContent(); self::assertStringContainsString('Willkommen
', $german); self::assertMatchesRegularExpression('#]*>.*?Leistungen#s', $german); self::assertStringNotContainsString('Our services', $german); self::assertStringContainsString('>Zum Inhalt springen', $german); } public function testFormsSpeakTheLanguageOfThePageButSendInTheMainLanguage(): void { $this->languages->save(['en']); $forms = new FormRepository($this->db); $translations = ['en' => ['buttonLabel' => 'Send request', 'success' => 'Thanks!', 'fields' => [ 'art' => ['label' => 'Type of enquiry', 'options' => ['Beratung' => 'Advice', 'Angebot' => 'Quote', 'Gibt es nicht' => 'x']], 'email' => ['label' => 'Email', 'help' => ''], 'weg' => ['label' => 'Feld gibt es nicht'], ]]]; $fields = [ ['name' => 'art', 'type' => 'radio', 'label' => 'Art der Anfrage', 'required' => true, 'options' => ['Beratung', 'Angebot']], ['name' => 'email', 'type' => 'email', 'label' => 'E-Mail', 'required' => true, 'help' => 'Für die Antwort'], ]; try { $forms->save('anfrage', 'Anfrage', $fields, ['recipient' => 'info@example.org', 'translations' => ['xx' => []]], 0); self::fail('Eine unbekannte Sprache wird abgelehnt.'); } catch (\App\Domain\InvalidForm) { } $forms->save('anfrage', 'Anfrage', $fields, ['recipient' => 'info@example.org', 'translations' => $translations], 0); $stored = $forms->findBySlug('anfrage')['settings']['translations']['en']; self::assertSame(['art' => ['label' => 'Type of enquiry', 'options' => ['Beratung' => 'Advice', 'Angebot' => 'Quote']], 'email' => ['label' => 'Email']], $stored['fields'], 'Nur Felder und Optionen, die es gibt; leere fallen weg.'); $checked = $forms->check($forms->findBySlug('anfrage'), ['art' => 'Advice', 'email' => 'kaputt'], 'en'); self::assertSame(['art' => 'Please choose one of the options offered.', 'email' => 'Please enter a valid email address.'], $checked['errors'], 'Abgeschickt wird der Wert der Hauptsprache, nicht die Anzeige.'); $document = $this->document('Kontakt', ['en' => ['title' => 'Contact', 'slug' => 'contact']]); $document['blocks'][0]['children'][] = ['id' => 'f1', 'type' => 'form', 'props' => ['form' => 'anfrage']]; $this->publishedPage('kontakt', $document); $english = (string) $this->request('/en/contact')->getContent(); self::assertMatchesRegularExpression('#