From 33b49c5dbb2067c062796f8aeef79229e6bf3333 Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Fri, 11 Sep 2026 13:52:13 +0200 Subject: [PATCH] Add hand-rolled English support (/en/*) alongside the Italian site MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PocketBase gains optional, manually-authored English fields on articles and categories (same record, same slug), and the frontend serves an /en counterpart of every dynamic route via thin page wrappers around shared view components — no i18n library, consistent with the project's existing minimalism stance for a two-locale site with ~20 UI strings. An article/category with no translation 404s cleanly on its own /en detail page and is filtered out of /en listings, and the header's language-switch link falls back to the English blog index rather than a dead link; resolving that requires a global route middleware, since the layout's header renders before the page content in document order and so can't react to state a page component sets during its own async setup. --- CLAUDE.md | 21 +- docs/architecture.md | 4 + docs/content-model.md | 29 ++- docs/frontend.md | 122 ++++++++-- frontend/app/components/ArticleCard.vue | 13 +- frontend/app/components/LanguageSwitch.vue | 37 ++++ frontend/app/components/views/ArticleView.vue | 157 +++++++++++++ .../app/components/views/BlogIndexView.vue | 67 ++++++ .../app/components/views/CategoryView.vue | 75 +++++++ frontend/app/components/views/HomeView.vue | 200 +++++++++++++++++ frontend/app/composables/useLangSwitch.ts | 16 ++ frontend/app/composables/useLocale.ts | 7 + frontend/app/composables/useSeo.ts | 61 +++++ frontend/app/layouts/default.vue | 50 +++-- frontend/app/middleware/lang-switch.global.ts | 47 ++++ frontend/app/pages/blog/[slug].vue | 156 +------------ frontend/app/pages/blog/index.vue | 65 +----- frontend/app/pages/category/[slug].vue | 70 +----- frontend/app/pages/en/blog/[slug].vue | 3 + frontend/app/pages/en/blog/index.vue | 3 + frontend/app/pages/en/category/[slug].vue | 3 + frontend/app/pages/en/index.vue | 3 + frontend/app/pages/index.vue | 209 +----------------- frontend/nuxt.config.ts | 5 + .../routes/content/articles/[slug].get.ts | 44 +++- .../routes/content/articles/index.get.ts | 38 +++- .../routes/content/categories/[slug].get.ts | 19 +- .../routes/content/categories/index.get.ts | 20 +- frontend/server/utils/queries.ts | 10 +- frontend/shared/types/blog.ts | 2 + frontend/shared/utils/format.ts | 12 +- frontend/shared/utils/i18n.ts | 119 ++++++++++ frontend/shared/utils/locale.ts | 16 ++ .../1757500002_add_english_fields.js | 29 +++ tests/e2e/i18n.spec.ts | 51 +++++ tests/integration/api.test.ts | 45 ++++ tests/support/fixtures.ts | 8 +- tests/support/pocketbase.ts | 2 + tests/unit/format.utils.test.ts | 11 +- tests/unit/queries.utils.test.ts | 12 +- 40 files changed, 1285 insertions(+), 576 deletions(-) create mode 100644 frontend/app/components/LanguageSwitch.vue create mode 100644 frontend/app/components/views/ArticleView.vue create mode 100644 frontend/app/components/views/BlogIndexView.vue create mode 100644 frontend/app/components/views/CategoryView.vue create mode 100644 frontend/app/components/views/HomeView.vue create mode 100644 frontend/app/composables/useLangSwitch.ts create mode 100644 frontend/app/composables/useLocale.ts create mode 100644 frontend/app/composables/useSeo.ts create mode 100644 frontend/app/middleware/lang-switch.global.ts create mode 100644 frontend/app/pages/en/blog/[slug].vue create mode 100644 frontend/app/pages/en/blog/index.vue create mode 100644 frontend/app/pages/en/category/[slug].vue create mode 100644 frontend/app/pages/en/index.vue create mode 100644 frontend/shared/utils/i18n.ts create mode 100644 frontend/shared/utils/locale.ts create mode 100644 pocketbase/pb_migrations/1757500002_add_english_fields.js create mode 100644 tests/e2e/i18n.spec.ts diff --git a/CLAUDE.md b/CLAUDE.md index 898d275..d9a42d2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -46,10 +46,17 @@ dominio, un solo certificato TLS. Se cambi questa scelta di routing, spiega il t (`/api/files/...`, sola lettura, nessun'autenticazione richiesta né concessa). - Il Markdown dell'articolo è convertito in HTML **nell'endpoint**, non nel componente: il contenuto è già nell'HTML SSR e `marked` resta fuori dal bundle client. -- L'**interfaccia** è solo in italiano, stringhe statiche nei componenti (niente - `@nuxtjs/i18n`, niente file di traduzione): `` è fisso in - `nuxt.config.ts`. La traduzione per i visitatori stranieri è delegata all'estensione - Google Translate del browser, non è gestita dall'app. I **contenuti** restano monolingua. +- Il sito è **bilingue** (italiano, sorgente, e inglese), fatto in casa senza `@nuxtjs/i18n` né + file di traduzione runtime: due lingue, ~20 stringhe statiche, nessun plurale — la libreria + costerebbe più codice di quanto risolva (il fallback su contenuti non tradotti, la + localizzazione degli endpoint Nitro e gran parte della SEO restano comunque da scrivere a mano). + `` è dinamico (per pagina, via `useSeo`), non più fisso. Le pagine inglesi vivono sotto + prefisso `/en/*`, come wrapper sottili attorno agli stessi componenti "view" delle pagine + italiane — vedi `docs/frontend.md#english-content`. Le stringhe UI statiche vivono **solo** in + `frontend/shared/utils/i18n.ts`. Gli **articoli** hanno campi paralleli opzionali + (`titleEn`/`contentEn`/`coverAltEn`, `nameEn` su categorie), tradotti **a mano** dall'editor in + admin — nessuna traduzione automatica. Vedi `docs/content-model.md#english-content` per lo + schema e la politica di fallback (404 pulito sul dettaglio non tradotto, filtro nelle liste). - `pocketbase/pb_migrations/*.js` definisce collection e regole: `listRule`/`viewRule` pubblici solo su `articles` (solo pubblicati, via `publishedAt`) e `categories`; `createRule`/ `updateRule`/`deleteRule` sempre `null` (solo superuser). Qualsiasi permesso in più va motivato. @@ -107,8 +114,8 @@ richiede `sudo`; se mancano librerie di sistema per il browser, installarle a pa | Type | Campi | |---|---| -| Article | title, slug, content (Markdown), cover, coverAlt, category (rel), publishedAt, authorName | -| Category | name, slug | +| Article | title, slug, content (Markdown), cover, coverAlt, category (rel), publishedAt, authorName, titleEn, contentEn, coverAltEn | +| Category | name, slug, nameEn | Deliberatamente minimale: niente tag, nessun campo SEO separato. La meta description è ricavata dall'inizio del body (`summarise` in `frontend/server/utils/pocketbase.ts`), la data è @@ -120,7 +127,7 @@ codice applicativo. Le URL pubbliche usano lo **slug**, mai l'id del record. ## Frontend -Rotte: `/`, `/blog`, `/blog/[slug]`, `/category/[slug]`. +Rotte: `/`, `/blog`, `/blog/[slug]`, `/category/[slug]`, ciascuna con equivalente `/en/...`. - SSR o prerender per tutto ciò che è indicizzabile. Mai pagine blog client-only senza motivo scritto. - `