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>
This commit is contained in:
1 parent
c7705b7083
commit
e15b2517b1
51 files changed
+3045
-219
No files matched your search
@@ -114,7 +114,12 @@ final class PostRepository
|
||||
/**
|
||||
* @return array{posts: list<array>, total: int, pages: int, page: int}
|
||||
*/
|
||||
public function published(int $page = 1, ?string $termId = null, int $perPage = self::PER_PAGE, string $locale = 'de'): array
|
||||
/**
|
||||
* @param string|null $language eine weitere Sprache (ADR 111): Titel und Auszug kommen aus
|
||||
* der Übersetzung des Beitrags, sofern er eine hat, die Namen der
|
||||
* Kategorien aus den Einstellungen
|
||||
*/
|
||||
public function published(int $page = 1, ?string $termId = null, int $perPage = self::PER_PAGE, string $locale = 'de', ?string $language = null): array
|
||||
{
|
||||
$where = "p.website_id = ? AND p.locale = ? AND p.type = 'post' AND p.published_revision_id IS NOT NULL AND p.deleted_at IS NULL";
|
||||
$params = ['default', $locale];
|
||||
@@ -138,25 +143,33 @@ final class PostRepository
|
||||
// einzeln nach seinem Bild und seinen Begriffen, eine Liste mit zwölf Einträgen kostete
|
||||
// also 24 Abfragen statt zwei. Bei neighbours(), das 500 Beiträge lädt, war das absurd.
|
||||
$images = $this->media->findMany(array_map(static fn (array $row) => (string) $row['featured_media_id'], $rows));
|
||||
$terms = $this->termsForMany(array_column($rows, 'id'));
|
||||
$terms = $this->termsForMany(array_column($rows, 'id'), $language);
|
||||
|
||||
return [
|
||||
'posts' => array_map(fn (array $row) => $this->present($row, $images, $terms[$row['id']] ?? ['category' => [], 'tag' => []]), $rows),
|
||||
'posts' => array_map(fn (array $row) => $this->present($row, $images, $terms[$row['id']] ?? ['category' => [], 'tag' => []], $language), $rows),
|
||||
'total' => $total,
|
||||
'pages' => $pages,
|
||||
'page' => $page,
|
||||
];
|
||||
}
|
||||
|
||||
private function present(array $row, array $images, array $terms): array
|
||||
private function present(array $row, array $images, array $terms, ?string $language = null): array
|
||||
{
|
||||
$document = json_decode($row['document'], true, 512, JSON_THROW_ON_ERROR);
|
||||
$item = $images[$row['featured_media_id']] ?? null;
|
||||
// Übersetzt ist ein Beitrag, wenn seine veröffentlichte Fassung die Sprache trägt. Sonst
|
||||
// steht er in der Hauptsprache in der Liste - wie jedes nicht übersetzte Feld.
|
||||
$translated = null !== $language && in_array($language, Translations::locales($document), true);
|
||||
$excerpt = $row['excerpt'] ?? $document['description'] ?? '';
|
||||
if ($translated) {
|
||||
$excerpt = $document['translations'][$language]['description'] ?? $excerpt;
|
||||
$document = Translations::apply($document, $language);
|
||||
}
|
||||
|
||||
return [
|
||||
'title' => $document['title'] ?? $row['path'],
|
||||
'href' => $this->settings->publicPath($row['path']),
|
||||
'excerpt' => $row['excerpt'] ?? $document['description'] ?? '',
|
||||
'excerpt' => $excerpt,
|
||||
'author' => $row['author'],
|
||||
'publishedAt' => $row['published_at'],
|
||||
'image' => null === $item ? null : ['alt' => $item['alt'] ?? '', 'variants' => $item['variants']],
|
||||
@@ -171,7 +184,7 @@ final class PostRepository
|
||||
*
|
||||
* @return array<string, array<string, list<array{slug: string, name: string, href: string}>>>
|
||||
*/
|
||||
private function termsForMany(array $pageIds): array
|
||||
private function termsForMany(array $pageIds, ?string $language = null): array
|
||||
{
|
||||
$pageIds = array_values(array_unique(array_filter($pageIds)));
|
||||
if ([] === $pageIds) {
|
||||
@@ -184,12 +197,28 @@ final class PostRepository
|
||||
$pageIds,
|
||||
) as $row) {
|
||||
$terms[$row['page_id']] ??= ['category' => [], 'tag' => []];
|
||||
$terms[$row['page_id']][$row['kind']][] = ['slug' => $row['slug'], 'name' => $row['name'], 'href' => '/'.self::ARCHIVE_PREFIX[$row['kind']].'/'.$row['slug']];
|
||||
$terms[$row['page_id']][$row['kind']][] = ['slug' => $row['slug'], 'name' => $this->termName($row, $language), 'href' => '/'.self::ARCHIVE_PREFIX[$row['kind']].'/'.$row['slug']];
|
||||
}
|
||||
|
||||
return $terms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Der Name einer Kategorie oder eines Schlagworts in einer weiteren Sprache, gepflegt unter
|
||||
* Einstellungen → Sprachen (ADR 111). Ohne Übersetzung der Name der Hauptsprache.
|
||||
*
|
||||
* @param array{kind: string, slug: string, name: string} $term
|
||||
*/
|
||||
public function termName(array $term, ?string $language): string
|
||||
{
|
||||
if (null === $language) {
|
||||
return $term['name'];
|
||||
}
|
||||
$name = $this->settings->load()['termNames'][$language][$term['kind'].':'.$term['slug']] ?? '';
|
||||
|
||||
return is_string($name) && '' !== $name ? $name : $term['name'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, list<array{slug: string, name: string, href: string}>>
|
||||
*/
|
||||
@@ -203,14 +232,14 @@ final class PostRepository
|
||||
*
|
||||
* @return array{previous: ?array, next: ?array}
|
||||
*/
|
||||
public function neighbours(string $pageId, string $locale = 'de'): array
|
||||
public function neighbours(string $pageId, string $locale = 'de', ?string $language = null): array
|
||||
{
|
||||
$termId = (string) $this->db->fetchOne(
|
||||
'SELECT t.id FROM term t JOIN page_term pt ON pt.term_id = t.id WHERE pt.page_id = ? AND t.kind = ? ORDER BY t.name LIMIT 1',
|
||||
[$pageId, 'category'],
|
||||
);
|
||||
// Dieselbe Sortierung wie im Archiv, damit „vorheriges" dort dasselbe heißt wie hier.
|
||||
$all = $this->published(1, '' === $termId ? null : $termId, 500, $locale)['posts'];
|
||||
$all = $this->published(1, '' === $termId ? null : $termId, 500, $locale, $language)['posts'];
|
||||
// present() gibt bewusst keine Kennung heraus - in eine Liste gehören Titel und Adresse.
|
||||
// Verglichen wird deshalb über die Adresse, die innerhalb einer Website eindeutig ist.
|
||||
$own = $this->settings->publicPath((string) $this->db->fetchOne('SELECT path FROM page WHERE id = ?', [$pageId]));
|
||||
@@ -249,11 +278,11 @@ final class PostRepository
|
||||
|
||||
private ?bool $anyPublished = null;
|
||||
|
||||
public function termsFor(string $pageId): array
|
||||
public function termsFor(string $pageId, ?string $language = null): array
|
||||
{
|
||||
$terms = ['category' => [], 'tag' => []];
|
||||
foreach ($this->db->fetchAllAssociative('SELECT t.kind, t.slug, t.name FROM term t JOIN page_term pt ON pt.term_id = t.id WHERE pt.page_id = ? ORDER BY t.name', [$pageId]) as $row) {
|
||||
$terms[$row['kind']][] = ['slug' => $row['slug'], 'name' => $row['name'], 'href' => '/'.self::ARCHIVE_PREFIX[$row['kind']].'/'.$row['slug']];
|
||||
$terms[$row['kind']][] = ['slug' => $row['slug'], 'name' => $this->termName($row, $language), 'href' => '/'.self::ARCHIVE_PREFIX[$row['kind']].'/'.$row['slug']];
|
||||
}
|
||||
|
||||
return $terms;
|
||||
|
||||
Reference in new issue
Block a user