Add hand-rolled English support (/en/*) alongside the Italian site
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.
This commit is contained in:
@@ -1,13 +1,17 @@
|
||||
import type { Locale } from './locale'
|
||||
|
||||
const INTL_LOCALE: Record<Locale, string> = { it: 'it-IT', en: 'en-GB' }
|
||||
|
||||
/** Human-readable date for display; returns an empty string when unset. */
|
||||
export function formatDate(value: string | null | undefined): string {
|
||||
export function formatDate(value: string | null | undefined, locale: Locale = 'it'): string {
|
||||
if (!value) return ''
|
||||
return new Intl.DateTimeFormat('it-IT', { dateStyle: 'long' }).format(new Date(value))
|
||||
return new Intl.DateTimeFormat(INTL_LOCALE[locale], { dateStyle: 'long' }).format(new Date(value))
|
||||
}
|
||||
|
||||
/** Date and time of publication, shown in the article byline. */
|
||||
export function formatDateTime(value: string | null | undefined): string {
|
||||
export function formatDateTime(value: string | null | undefined, locale: Locale = 'it'): string {
|
||||
if (!value) return ''
|
||||
return new Intl.DateTimeFormat('it-IT', { dateStyle: 'long', timeStyle: 'short' })
|
||||
return new Intl.DateTimeFormat(INTL_LOCALE[locale], { dateStyle: 'long', timeStyle: 'short' })
|
||||
.format(new Date(value))
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Static UI copy, in both languages. Deliberately a plain object, not a
|
||||
* library: two locales, ~20 short strings, no plurals — see docs/frontend.md.
|
||||
* Keyed by role (not by the Italian text), so a wording tweak never touches
|
||||
* the key.
|
||||
*/
|
||||
import type { Locale } from './locale'
|
||||
|
||||
export const STRINGS = {
|
||||
it: {
|
||||
skipLink: 'Vai al contenuto',
|
||||
tagline: 'Articoli, guide e analisi.',
|
||||
navAriaLabel: 'Navigazione principale',
|
||||
navHome: 'Home',
|
||||
navBlog: 'Tutti gli articoli',
|
||||
navCorralito: 'Come difendersi dal corralito',
|
||||
footerNavAriaLabel: 'Naviga',
|
||||
footerNavigaKicker: 'Naviga',
|
||||
footerSeguiciKicker: 'Seguici',
|
||||
footerFollowAriaLabel: 'Seguici',
|
||||
contactLabel: 'Contatti:',
|
||||
copyright: 'Tutti i diritti riservati.',
|
||||
langSwitchLabel: 'English',
|
||||
home: {
|
||||
heading: "La fine dell'Unione Europea per come l'abbiamo conosciuta.",
|
||||
img1Alt: 'Manifesti e scritte contro le banche durante una crisi bancaria',
|
||||
img2Alt: "Bandiera dell'Unione Europea incrinata",
|
||||
p1: "La fine della nostra moneta come moneta forte: la fine di quella moneta che ci ha dato la vera libertà di viaggiare, consumare e fare.",
|
||||
p2: "Una fine che avverrà a seguito di un \"incidente\" nucleare, al quale farà seguito un corralito bancario, ossia l'impossibilità, da parte dei cittadini europei, di disporre liberamente dei propri depositi.",
|
||||
p3: "Le monete fiduciarie sono per loro natura cannibalizzanti. Oggi tocca all'euro. Anche perché le altre hanno praticamente perso tutte tutto.",
|
||||
fallbackKicker: 'Ultimo',
|
||||
readLatest: "Leggi l'ultimo articolo",
|
||||
metaDescription: 'Articoli, guide e analisi.',
|
||||
},
|
||||
blog: {
|
||||
archiveKicker: "L'archivio",
|
||||
title: 'Articoli',
|
||||
titlePage: 'Articoli — pagina {page}',
|
||||
metaDescription: 'Tutti gli articoli pubblicati sul blog.',
|
||||
loadError: 'Non è stato possibile caricare gli articoli. Riprova più tardi.',
|
||||
loading: 'Caricamento…',
|
||||
empty: 'Non è stato ancora pubblicato nulla.',
|
||||
prevPage: 'Pagina precedente',
|
||||
nextPage: 'Pagina successiva',
|
||||
pageOf: 'Pagina {current} di {total}',
|
||||
paginationAriaLabel: 'Paginazione',
|
||||
},
|
||||
category: {
|
||||
kicker: 'Categoria',
|
||||
metaDescription: 'Tutti gli articoli nella categoria {name}.',
|
||||
loadError: 'Non è stato possibile caricare gli articoli. Riprova più tardi.',
|
||||
empty: 'Non ci sono articoli in questa categoria.',
|
||||
},
|
||||
article: {
|
||||
breadcrumbAriaLabel: 'Percorso di navigazione',
|
||||
articlesCrumb: 'Articoli',
|
||||
byAuthor: 'Di {author}',
|
||||
backToArticles: 'Torna a tutti gli articoli',
|
||||
},
|
||||
},
|
||||
en: {
|
||||
skipLink: 'Skip to content',
|
||||
tagline: 'Articles, guides and analysis.',
|
||||
navAriaLabel: 'Main navigation',
|
||||
navHome: 'Home',
|
||||
navBlog: 'All articles',
|
||||
navCorralito: '',
|
||||
footerNavAriaLabel: 'Navigate',
|
||||
footerNavigaKicker: 'Navigate',
|
||||
footerSeguiciKicker: 'Follow us',
|
||||
footerFollowAriaLabel: 'Follow us',
|
||||
contactLabel: 'Contact:',
|
||||
copyright: 'All rights reserved.',
|
||||
langSwitchLabel: 'Italiano',
|
||||
home: {
|
||||
heading: "The end of the European Union as we've known it.",
|
||||
img1Alt: 'Banners and graffiti against banks during a banking crisis',
|
||||
img2Alt: 'Cracked European Union flag',
|
||||
p1: 'The end of our currency as a strong currency: the end of the currency that gave us the real freedom to travel, spend and act.',
|
||||
p2: 'An end that will follow a nuclear "incident", followed by a banking corralito — the impossibility, for European citizens, of freely accessing their own deposits.',
|
||||
p3: "Fiat currencies are cannibalizing by nature. Today it's the euro's turn. Especially since the others have already lost almost everything.",
|
||||
fallbackKicker: 'Latest',
|
||||
readLatest: 'Read the latest article',
|
||||
metaDescription: 'Articles, guides and analysis.',
|
||||
},
|
||||
blog: {
|
||||
archiveKicker: 'The archive',
|
||||
title: 'Articles',
|
||||
titlePage: 'Articles — page {page}',
|
||||
metaDescription: 'All the articles published on the blog.',
|
||||
loadError: 'Could not load the articles. Please try again later.',
|
||||
loading: 'Loading…',
|
||||
empty: 'Nothing has been published yet.',
|
||||
prevPage: 'Previous page',
|
||||
nextPage: 'Next page',
|
||||
pageOf: 'Page {current} of {total}',
|
||||
paginationAriaLabel: 'Pagination',
|
||||
},
|
||||
category: {
|
||||
kicker: 'Category',
|
||||
metaDescription: 'All the articles in the {name} category.',
|
||||
loadError: 'Could not load the articles. Please try again later.',
|
||||
empty: 'There are no articles in this category.',
|
||||
},
|
||||
article: {
|
||||
breadcrumbAriaLabel: 'Breadcrumb',
|
||||
articlesCrumb: 'Articles',
|
||||
byAuthor: 'By {author}',
|
||||
backToArticles: 'Back to all articles',
|
||||
},
|
||||
},
|
||||
} as const satisfies Record<Locale, unknown>
|
||||
|
||||
/** Fills `{placeholder}` tokens in a translated string. */
|
||||
export function interpolate(template: string, params: Record<string, string | number>): string {
|
||||
return template.replace(/\{(\w+)\}/g, (match, key: string) =>
|
||||
key in params ? String(params[key]) : match
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
export type Locale = 'it' | 'en'
|
||||
|
||||
export const LOCALES: Locale[] = ['it', 'en']
|
||||
|
||||
/** Prefixes a bare, unprefixed path (e.g. `/blog/mio-slug`) for the given locale. */
|
||||
export function localePath(locale: Locale, path: string): string {
|
||||
if (locale !== 'en') return path
|
||||
return path === '/' ? '/en' : `/en${path}`
|
||||
}
|
||||
|
||||
/** Strips the `/en` prefix, if any, returning the bare Italian-equivalent path. */
|
||||
export function bareLocalePath(path: string): string {
|
||||
if (path === '/en') return '/'
|
||||
if (path.startsWith('/en/')) return path.slice(3)
|
||||
return path
|
||||
}
|
||||
Reference in New Issue
Block a user