Add an English translation of the corralito training-courses page

It was deliberately left Italian-only when /en support was first added;
now translated and following the same thin-page + shared-view pattern
as every other route, with the nav link and language switch no longer
special-casing it.
This commit is contained in:
2026-09-11 15:39:31 +02:00
parent 1247a2ca07
commit ac536bd152
8 changed files with 173 additions and 148 deletions
+5 -4
View File
@@ -8,7 +8,7 @@
| `/blog` | `/en/blog` | `app/pages/blog/index.vue` + `app/pages/en/blog/index.vue``BlogIndexView.vue` | Paginated archive (`PAGE_SIZE = 12`), grid of `ArticleCard`, prev/next via `?page=`. |
| `/blog/[slug]` | `/en/blog/[slug]` | `app/pages/blog/[slug].vue` + `app/pages/en/blog/[slug].vue``ArticleView.vue` | Full article: fetches `/content/articles/:slug`, renders the pre-converted `article.html`, SEO meta, canonical URL, Open Graph, JSON-LD `BlogPosting`, breadcrumb to its category. |
| `/category/[slug]` | `/en/category/[slug]` | `app/pages/category/[slug].vue` + `app/pages/en/category/[slug].vue``CategoryView.vue` | Fetches the category by slug, then a paginated, category-filtered article list; 404s if the category doesn't exist. |
| `/come-difendersi-dal-corralito` | *(none)* | `app/pages/come-difendersi-dal-corralito.vue` | Fully static marketing page, no PocketBase data, Italian only — see [English content](#english-content-1). |
| `/come-difendersi-dal-corralito` | `/en/come-difendersi-dal-corralito` | `app/pages/come-difendersi-dal-corralito.vue` + `app/pages/en/come-difendersi-dal-corralito.vue``CorralitoView.vue` | Fully static marketing page, no PocketBase data — copy lives in `STRINGS[locale].corralito` (`shared/utils/i18n.ts`), same pattern as every other view. |
All pages are SSR (`useFetch`/`useSeoMeta`); nothing blog-related is client-only-rendered.
@@ -47,8 +47,9 @@ Nitro locale-awareness, and most of the SEO work are custom code either way).
by the time a page component's own `<script setup>` would set that state, the header has already
resolved and rendered with whatever the default was — middleware runs, and fully resolves,
before the layout/page tree starts rendering at all, which is the only way to avoid that race.
When unavailable, the switch falls back to the other language's blog index (articles/categories)
or home page (the static corralito page, which has no English version at all).
When unavailable, the switch falls back to the other language's blog index/category list; every
other route (including the static corralito page) exists in both languages unconditionally, so
the mechanical `/en` prefix swap is always valid there.
- **SEO**: `app/composables/useSeo.ts` centralises canonical URL, Open Graph, JSON-LD, and
`hreflang` alternate links (`it`, `en`, `x-default`) for a given `{ locale, path, ... }``path`
is always the bare, unprefixed path, so IT/EN pages for the same content always agree on each
@@ -166,7 +167,7 @@ layout shift results.
render its own `<a>``RouterLink`'s built-in active-class/`aria-current` detection compares
only the route's `path`, not its query string, so with plain `NuxtLink`s every `?page=N` link
would end up (wrongly) marked as the current page.
- `app/components/views/*.vue` (`HomeView`, `BlogIndexView`, `ArticleView`, `CategoryView`) — the
- `app/components/views/*.vue` (`HomeView`, `BlogIndexView`, `ArticleView`, `CategoryView`, `CorralitoView`) — the
actual page markup/logic, parametrized by a `locale` prop; the real `pages/**` and `pages/en/**`
files are thin wrappers around these (see [English content](#english-content)). Registered
without Nuxt's default nested-directory name prefix
@@ -0,0 +1,110 @@
<script setup lang="ts">
import type { Locale } from '#shared/utils/locale'
import { STRINGS } from '#shared/utils/i18n'
const props = defineProps<{ locale: Locale }>()
const strings = computed(() => STRINGS[props.locale])
const { public: config } = useRuntimeConfig()
useSeo({
locale: props.locale,
path: '/come-difendersi-dal-corralito',
title: strings.value.corralito.metaTitle,
description: strings.value.corralito.metaDescription,
image: `${config.siteUrl}/protesta-bancaria.jpg`,
})
</script>
<template>
<article>
<header class="article-header">
<h1>{{ strings.navCorralito }}</h1>
</header>
<img
class="cover"
src="/protesta-bancaria.jpg"
:alt="strings.corralito.coverAlt"
width="770"
height="420"
>
<div class="prose">
<p>{{ strings.corralito.p1 }}</p>
<p>{{ strings.corralito.p2 }}</p>
<p>{{ strings.corralito.topicsIntro }}</p>
<ul>
<li v-for="topic in strings.corralito.topics" :key="topic">{{ topic }}</li>
</ul>
<p>{{ strings.corralito.disclaimer }}</p>
</div>
<div class="contact-cta">
<p class="kicker">{{ strings.corralito.contactKicker }}</p>
<p class="contact-links">
<a :href="`mailto:${SITE_EMAIL}`">{{ strings.corralito.emailLink }}</a>
<span aria-hidden="true">·</span>
<a href="https://t.me/Ito1505" target="_blank" rel="noopener">{{ strings.corralito.telegramLink }}</a>
</p>
</div>
</article>
</template>
<style scoped>
.article-header {
max-width: 46rem;
margin: 0 auto 2rem;
text-align: center;
}
@media (min-width: 48rem) {
.article-header {
margin-bottom: 3rem;
}
}
.cover {
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
margin-block: 0 3rem;
background: var(--color-surface);
}
.prose {
margin-inline: auto;
}
.contact-cta {
max-width: var(--width-prose);
margin: 3rem auto 0;
padding-top: 2rem;
border-top: 1px solid var(--color-border);
text-align: center;
}
.contact-links {
margin-top: 0.75rem;
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 0.6rem;
font-size: 0.75rem;
font-weight: 600;
letter-spacing: 0.14em;
text-transform: uppercase;
}
.contact-links a {
padding-bottom: 0.35rem;
text-decoration: none;
border-bottom: 1px solid var(--color-ink);
}
.contact-links a:hover {
color: var(--color-ink);
}
</style>
+4 -4
View File
@@ -27,8 +27,8 @@ const strings = computed(() => STRINGS[locale.value])
<ul>
<li><NuxtLink :to="localePath(locale, '/')">{{ strings.navHome }}</NuxtLink></li>
<li><NuxtLink :to="localePath(locale, '/blog')">{{ strings.navBlog }}</NuxtLink></li>
<li v-if="locale === 'it'">
<NuxtLink to="/come-difendersi-dal-corralito">{{ strings.navCorralito }}</NuxtLink>
<li>
<NuxtLink :to="localePath(locale, '/come-difendersi-dal-corralito')">{{ strings.navCorralito }}</NuxtLink>
</li>
</ul>
</div>
@@ -55,8 +55,8 @@ const strings = computed(() => STRINGS[locale.value])
<ul>
<li><NuxtLink :to="localePath(locale, '/')">{{ strings.navHome }}</NuxtLink></li>
<li><NuxtLink :to="localePath(locale, '/blog')">{{ strings.navBlog }}</NuxtLink></li>
<li v-if="locale === 'it'">
<NuxtLink to="/come-difendersi-dal-corralito">{{ strings.navCorralito }}</NuxtLink>
<li>
<NuxtLink :to="localePath(locale, '/come-difendersi-dal-corralito')">{{ strings.navCorralito }}</NuxtLink>
</li>
</ul>
</nav>
@@ -38,10 +38,5 @@ export default defineNuxtRouteMiddleware(async (to) => {
return
}
if (bare === '/come-difendersi-dal-corralito') {
langSwitch.value = { available: false, fallback: '/en' }
return
}
langSwitch.value = { available: true, fallback: '/en' }
})
@@ -1,132 +1,3 @@
<script setup lang="ts">
const { public: config } = useRuntimeConfig()
const route = useRoute()
const canonical = computed(() => `${config.siteUrl}${route.path}`)
const title = 'Corsi di formazione sulle criptovalute'
const description = 'Corsi pratici, individuali o di gruppo, per capire come funzionano le criptovalute e difendersi dal corralito.'
useSeoMeta({
title,
description,
ogTitle: title,
ogDescription: description,
ogType: 'website',
ogUrl: canonical,
ogImage: `${config.siteUrl}/protesta-bancaria.jpg`,
})
useHead({ link: [{ rel: 'canonical', href: canonical }] })
</script>
<template>
<article>
<header class="article-header">
<h1>Come difendersi dal corralito</h1>
</header>
<img
class="cover"
src="/protesta-bancaria.jpg"
alt="Manifesti e scritte contro le banche durante una crisi bancaria"
width="770"
height="420"
>
<div class="prose">
<p>
Stai pensando di avvicinarti al mondo delle criptovalute (un mercato parallelo al
mercato tradizionale finanziario), ma non sai da dove cominciare?
</p>
<p>
È possibile organizzare corsi individuali o di gruppo dedicati alla conoscenza e al
funzionamento delle criptovalute, con un approccio prevalentemente pratico e senza
fornire consigli o raccomandazioni finanziarie.
</p>
<p>Durante il corso possiamo affrontare, tra gli altri, questi argomenti:</p>
<ul>
<li>quali sono i principali tipi di criptovalute e quali differenze esistono tra loro;</li>
<li>come funziona una blockchain e cosa significa realmente effettuare una transazione;</li>
<li>dove e come vengono conservate le criptovalute: wallet, exchange e sistemi di custodia;</li>
<li>come acquistare e vendere criptovalute e quali sono le principali modalità disponibili;</li>
<li>come utilizzare un wallet e gestire correttamente le proprie chiavi;</li>
<li>quali sono i principali rischi legati alla sicurezza e alla custodia;</li>
<li>come orientarsi nel vastissimo universo delle criptovalute e dei token.</li>
</ul>
<p>
Non si tratta di consulenza finanziaria e non verranno fornite indicazioni su quali
criptovalute acquistare o vendere. L'obiettivo è fornire conoscenze e strumenti per
comprendere questo nuovo mondo e muoversi al suo interno con maggiore consapevolezza.
</p>
</div>
<div class="contact-cta">
<p class="kicker">Per informazioni sui corsi, modalità e disponibilità</p>
<p class="contact-links">
<a :href="`mailto:${SITE_EMAIL}`">Scrivimi via email</a>
<span aria-hidden="true">·</span>
<a href="https://t.me/Ito1505" target="_blank" rel="noopener">Scrivimi su Telegram</a>
</p>
</div>
</article>
<CorralitoView locale="it" />
</template>
<style scoped>
.article-header {
max-width: 46rem;
margin: 0 auto 2rem;
text-align: center;
}
@media (min-width: 48rem) {
.article-header {
margin-bottom: 3rem;
}
}
.cover {
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
margin-block: 0 3rem;
background: var(--color-surface);
}
.prose {
margin-inline: auto;
}
.contact-cta {
max-width: var(--width-prose);
margin: 3rem auto 0;
padding-top: 2rem;
border-top: 1px solid var(--color-border);
text-align: center;
}
.contact-links {
margin-top: 0.75rem;
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 0.6rem;
font-size: 0.75rem;
font-weight: 600;
letter-spacing: 0.14em;
text-transform: uppercase;
}
.contact-links a {
padding-bottom: 0.35rem;
text-decoration: none;
border-bottom: 1px solid var(--color-ink);
}
.contact-links a:hover {
color: var(--color-ink);
}
</style>
@@ -0,0 +1,3 @@
<template>
<CorralitoView locale="en" />
</template>
+43 -1
View File
@@ -62,6 +62,27 @@ export const STRINGS = {
byAuthor: 'Di {author}',
backToArticles: 'Torna a tutti gli articoli',
},
corralito: {
metaTitle: 'Corsi di formazione sulle criptovalute',
metaDescription: 'Corsi pratici, individuali o di gruppo, per capire come funzionano le criptovalute e difendersi dal corralito.',
coverAlt: 'Manifesti e scritte contro le banche durante una crisi bancaria',
p1: 'Stai pensando di avvicinarti al mondo delle criptovalute (un mercato parallelo al mercato tradizionale finanziario), ma non sai da dove cominciare?',
p2: "È possibile organizzare corsi individuali o di gruppo dedicati alla conoscenza e al funzionamento delle criptovalute, con un approccio prevalentemente pratico e senza fornire consigli o raccomandazioni finanziarie.",
topicsIntro: 'Durante il corso possiamo affrontare, tra gli altri, questi argomenti:',
topics: [
'quali sono i principali tipi di criptovalute e quali differenze esistono tra loro;',
'come funziona una blockchain e cosa significa realmente effettuare una transazione;',
'dove e come vengono conservate le criptovalute: wallet, exchange e sistemi di custodia;',
'come acquistare e vendere criptovalute e quali sono le principali modalità disponibili;',
'come utilizzare un wallet e gestire correttamente le proprie chiavi;',
'quali sono i principali rischi legati alla sicurezza e alla custodia;',
'come orientarsi nel vastissimo universo delle criptovalute e dei token.',
],
disclaimer: "Non si tratta di consulenza finanziaria e non verranno fornite indicazioni su quali criptovalute acquistare o vendere. L'obiettivo è fornire conoscenze e strumenti per comprendere questo nuovo mondo e muoversi al suo interno con maggiore consapevolezza.",
contactKicker: 'Per informazioni sui corsi, modalità e disponibilità',
emailLink: 'Scrivimi via email',
telegramLink: 'Scrivimi su Telegram',
},
},
en: {
skipLink: 'Skip to content',
@@ -69,7 +90,7 @@ export const STRINGS = {
navAriaLabel: 'Main navigation',
navHome: 'Home',
navBlog: 'All articles',
navCorralito: '',
navCorralito: 'How to protect yourself from the corralito',
footerNavAriaLabel: 'Navigate',
footerNavigaKicker: 'Navigate',
footerSeguiciKicker: 'Follow us',
@@ -118,6 +139,27 @@ export const STRINGS = {
byAuthor: 'By {author}',
backToArticles: 'Back to all articles',
},
corralito: {
metaTitle: 'Cryptocurrency training courses',
metaDescription: 'Practical, individual or group courses to understand how cryptocurrencies work and protect yourself from the corralito.',
coverAlt: 'Banners and graffiti against banks during a banking crisis',
p1: "Are you thinking about getting into the world of cryptocurrencies (a market parallel to the traditional financial market), but don't know where to start?",
p2: 'I can organize individual or group courses dedicated to understanding how cryptocurrencies work, with a mostly hands-on approach and without giving financial advice or recommendations.',
topicsIntro: 'During the course we can cover, among others, these topics:',
topics: [
'the main types of cryptocurrencies and the differences between them;',
'how a blockchain works and what actually happens when you make a transaction;',
'where and how cryptocurrencies are stored: wallets, exchanges and custody systems;',
'how to buy and sell cryptocurrencies and the main methods available;',
'how to use a wallet and correctly manage your own keys;',
'the main risks related to security and custody;',
'how to find your way around the vast universe of cryptocurrencies and tokens.',
],
disclaimer: 'This is not financial advice, and no indications will be given on which cryptocurrencies to buy or sell. The goal is to provide knowledge and tools to understand this new world and navigate it with greater awareness.',
contactKicker: 'For information on courses, format and availability',
emailLink: 'Email me',
telegramLink: 'Message me on Telegram',
},
},
} as const satisfies Record<Locale, unknown>
+7 -4
View File
@@ -41,11 +41,14 @@ test('language switch falls back to the English blog index for an untranslated a
await expect(languageSwitch).toHaveAttribute('href', '/en/blog')
})
test('language switch on the Italian-only corralito page falls back to the English home page', async ({
page,
}) => {
test('the corralito page has an English translation', async ({ page }) => {
await page.goto('/come-difendersi-dal-corralito')
const languageSwitch = page.getByRole('link', { name: 'English' })
await expect(languageSwitch).toHaveAttribute('href', '/en')
await expect(languageSwitch).toHaveAttribute('href', '/en/come-difendersi-dal-corralito')
await page.goto('/en/come-difendersi-dal-corralito')
await expect(
page.getByRole('heading', { level: 1, name: 'How to protect yourself from the corralito' })
).toBeVisible()
})