diff --git a/astro.config.mjs b/astro.config.mjs index cae0a10..4a6f308 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -2,8 +2,7 @@ import { defineConfig } from 'astro/config'; import tailwindcss from '@tailwindcss/vite'; import sitemap from '@astrojs/sitemap'; -import { readdirSync, existsSync } from 'node:fs'; -import { getScope, resolveScopeId, SCOPES } from './src/config/scopes.ts'; +import { getScope, resolveScopeId } from './src/config/scopes.ts'; // One codebase, three real domains. SITE_SCOPE=nl|be|eu picks which one this build is; see // src/config/scopes.ts. Astro resolves `site` and `i18n.locales` at build time rather than per @@ -12,36 +11,6 @@ const scopeId = resolveScopeId(process.env.SITE_SCOPE); const scope = getScope(scopeId); console.log(`[scope] building ${scopeId} -> ${scope.site} [${scope.locales.join(', ')}]`); -// Locale routes exist as real directories under src/pages//, so i18n.locales does NOT -// gate them: a build whose scope drops a locale would still emit that locale's pages. Astro can -// only skip a route that is dynamic, so until src/pages// becomes a [locale] route this -// mismatch has to fail loudly rather than ship a .eu site full of Dutch pages. -const ALL_LOCALES = new Set(Object.values(SCOPES).flatMap((s) => s.locales)); -const routeLocales = existsSync('./src/pages') - ? readdirSync('./src/pages', { withFileTypes: true }) - .filter((e) => e.isDirectory() && ALL_LOCALES.has(e.name)) - .map((e) => e.name) - : []; -const orphans = routeLocales.filter((l) => !scope.locales.includes(l)); -if (orphans.length) { - throw new Error( - `Scope "${scopeId}" declares locales [${scope.locales.join(', ')}], but src/pages/ still ` + - `contains route directories for [${orphans.join(', ')}], which this build would emit ` + - `anyway. Convert src/pages// to a [locale] dynamic route whose getStaticPaths ` + - `reads the scope before building this scope.`, - ); -} -// The mirror case: a scope declaring a locale that has no pages yet. A warning, not an error -- -// a scope may legitimately be defined before its translations exist -- but silence here would -// mean shipping .be with its French half quietly missing. -const missing = scope.locales.slice(1).filter((l) => !routeLocales.includes(l)); -if (missing.length) { - console.warn( - `[scope] WARNING: scope "${scopeId}" declares locale(s) [${missing.join(', ')}] with no ` + - `src/pages// directory. Those languages will be advertised but not served.`, - ); -} - // https://astro.build/config export default defineConfig({ site: scope.site, diff --git a/src/config/scopes.ts b/src/config/scopes.ts index 3b0fcaa..0c5ccaa 100644 --- a/src/config/scopes.ts +++ b/src/config/scopes.ts @@ -46,7 +46,10 @@ export const SCOPES: Record = { be: { site: 'https://atproto.be', countries: ['be'], - locales: ['en', 'nl', 'fr'], + // French is the goal for .be and half its audience, but the UI string catalogue has no `fr` + // entries, so building it produces French URLs containing English -- worse than not offering + // it. Add 'fr' here in the same change that adds the French strings, not before. + locales: ['en', 'nl'], aggregate: false, relatedLangs: ['nl', 'fr'], joinCountries: ['be'], diff --git a/src/i18n/paths.ts b/src/i18n/paths.ts new file mode 100644 index 0000000..f7b747f --- /dev/null +++ b/src/i18n/paths.ts @@ -0,0 +1,44 @@ +// Locale routing for the scope-driven builds. +// +// Locale pages used to exist as real files (src/pages/about.astro + src/pages/nl/about.astro). +// That made `i18n.locales` decorative: a .eu build declaring English-only still emitted every +// Dutch page, because Astro cannot skip a route that exists on disk. Only a DYNAMIC route can +// opt out, which is what this enables — one `src/pages/[...lang]/about.astro` whose +// getStaticPaths asks the scope which locales to build. +// +// The rest parameter is what keeps the default locale at the root: `lang: undefined` produces +// /about, `lang: 'nl'` produces /nl/about. That matches `prefixDefaultLocale: false`. + +import { getScope } from '../config/scopes'; + +const scope = getScope(import.meta.env.PUBLIC_SITE_SCOPE); + +export const LOCALES = scope.locales; +export const DEFAULT_LOCALE = scope.locales[0]; + +export type Lang = string; + +/** getStaticPaths() for every localized page. */ +export function localePaths() { + return LOCALES.map((locale, i) => ({ + // The default locale takes the bare path; the rest are prefixed. + params: { lang: i === 0 ? undefined : locale }, + props: { lang: locale }, + })); +} + +/** Resolve the locale for a request, tolerating a missing rest param. */ +export function langOf(params: { lang?: string }): Lang { + return params.lang ?? DEFAULT_LOCALE; +} + +/** Pick a value for the current locale, falling back to the default locale's value. + * + * A scope can declare a locale before its copy exists (.be declares fr today). Falling back is + * better than an empty , but it is a stopgap: untranslated pages should be visible, so + * missing keys warn at build time rather than passing silently. */ +export function t<T>(table: Record<string, T>, lang: Lang, what = 'value'): T { + if (lang in table) return table[lang]; + console.warn(`[i18n] missing ${what} for locale "${lang}"; falling back to "${DEFAULT_LOCALE}"`); + return table[DEFAULT_LOCALE]; +} diff --git a/src/pages/[...lang]/about.astro b/src/pages/[...lang]/about.astro new file mode 100644 index 0000000..a731ec7 --- /dev/null +++ b/src/pages/[...lang]/about.astro @@ -0,0 +1,11 @@ +--- +import AboutPage from '../../components/pages/AboutPage.astro'; +import { localePaths } from '../../i18n/paths'; + +// One route per locale the scope declares; a scope that drops a locale simply does not build it. +export const getStaticPaths = localePaths; + +const { lang } = Astro.props; +--- + +<AboutPage lang={lang} /> diff --git a/src/pages/[...lang]/builders.astro b/src/pages/[...lang]/builders.astro new file mode 100644 index 0000000..534e536 --- /dev/null +++ b/src/pages/[...lang]/builders.astro @@ -0,0 +1,11 @@ +--- +import BuildersPage from '../../components/pages/BuildersPage.astro'; +import { localePaths } from '../../i18n/paths'; + +// One route per locale the scope declares; a scope that drops a locale simply does not build it. +export const getStaticPaths = localePaths; + +const { lang } = Astro.props; +--- + +<BuildersPage lang={lang} /> diff --git a/src/pages/[...lang]/community-size.astro b/src/pages/[...lang]/community-size.astro new file mode 100644 index 0000000..3b255e2 --- /dev/null +++ b/src/pages/[...lang]/community-size.astro @@ -0,0 +1,30 @@ +--- +import Base from '../../layouts/Base.astro'; +import CommunitySizePage from '../../components/pages/CommunitySizePage.astro'; +import { nlStats } from '../../data/nl-stats'; +import { localePaths, t } from '../../i18n/paths'; + +export const getStaticPaths = localePaths; + +const { lang } = Astro.props; + +// The modelled figure stays in the metadata even though the page leads with the counted +// one: it is the number that gets quoted, and it should travel with its ~ attached. +const est = nlStats.estimate.modelledNl.toLocaleString(lang === 'nl' ? 'nl-NL' : 'en-GB'); + +// The only page whose <title>/<description> differ per locale beyond what the component +// handles. Kept next to the route rather than in a global message catalogue so the copy stays +// visible to whoever edits the page. +const TITLES: Record<string, string> = { + en: 'How many Dutch accounts are on AT Protocol?', + nl: 'Hoeveel Nederlandse accounts zitten er op AT Protocol?', +}; +const DESCRIPTIONS: Record<string, string> = { + en: `AT Protocol has no country field, so we publish two numbers: the Dutch accounts we can point to, and a modelled total of around ${est}.`, + nl: `AT Protocol heeft geen landveld, dus publiceren we twee getallen: de Nederlandse accounts die we kunnen aanwijzen, en een gemodelleerd totaal van ongeveer ${est}.`, +}; +--- + +<Base lang={lang} title={t(TITLES, lang, 'title')} description={t(DESCRIPTIONS, lang, 'description')}> + <CommunitySizePage lang={lang} /> +</Base> diff --git a/src/pages/[...lang]/events.astro b/src/pages/[...lang]/events.astro new file mode 100644 index 0000000..3e887ef --- /dev/null +++ b/src/pages/[...lang]/events.astro @@ -0,0 +1,11 @@ +--- +import EventsPage from '../../components/pages/EventsPage.astro'; +import { localePaths } from '../../i18n/paths'; + +// One route per locale the scope declares; a scope that drops a locale simply does not build it. +export const getStaticPaths = localePaths; + +const { lang } = Astro.props; +--- + +<EventsPage lang={lang} /> diff --git a/src/pages/[...lang]/index.astro b/src/pages/[...lang]/index.astro new file mode 100644 index 0000000..93f17b2 --- /dev/null +++ b/src/pages/[...lang]/index.astro @@ -0,0 +1,11 @@ +--- +import HomePage from '../../components/pages/HomePage.astro'; +import { localePaths } from '../../i18n/paths'; + +// One route per locale the scope declares; a scope that drops a locale simply does not build it. +export const getStaticPaths = localePaths; + +const { lang } = Astro.props; +--- + +<HomePage lang={lang} /> diff --git a/src/pages/[...lang]/login.astro b/src/pages/[...lang]/login.astro new file mode 100644 index 0000000..ba390e7 --- /dev/null +++ b/src/pages/[...lang]/login.astro @@ -0,0 +1,11 @@ +--- +import LoginPage from '../../components/pages/LoginPage.astro'; +import { localePaths } from '../../i18n/paths'; + +// One route per locale the scope declares; a scope that drops a locale simply does not build it. +export const getStaticPaths = localePaths; + +const { lang } = Astro.props; +--- + +<LoginPage lang={lang} /> diff --git a/src/pages/[...lang]/members.astro b/src/pages/[...lang]/members.astro new file mode 100644 index 0000000..e13b354 --- /dev/null +++ b/src/pages/[...lang]/members.astro @@ -0,0 +1,11 @@ +--- +import MembersPage from '../../components/pages/MembersPage.astro'; +import { localePaths } from '../../i18n/paths'; + +// One route per locale the scope declares; a scope that drops a locale simply does not build it. +export const getStaticPaths = localePaths; + +const { lang } = Astro.props; +--- + +<MembersPage lang={lang} /> diff --git a/src/pages/[...lang]/privacy.astro b/src/pages/[...lang]/privacy.astro new file mode 100644 index 0000000..cb0101f --- /dev/null +++ b/src/pages/[...lang]/privacy.astro @@ -0,0 +1,11 @@ +--- +import PrivacyPage from '../../components/pages/PrivacyPage.astro'; +import { localePaths } from '../../i18n/paths'; + +// One route per locale the scope declares; a scope that drops a locale simply does not build it. +export const getStaticPaths = localePaths; + +const { lang } = Astro.props; +--- + +<PrivacyPage lang={lang} /> diff --git a/src/pages/[...lang]/terms.astro b/src/pages/[...lang]/terms.astro new file mode 100644 index 0000000..a7f68c5 --- /dev/null +++ b/src/pages/[...lang]/terms.astro @@ -0,0 +1,11 @@ +--- +import TermsPage from '../../components/pages/TermsPage.astro'; +import { localePaths } from '../../i18n/paths'; + +// One route per locale the scope declares; a scope that drops a locale simply does not build it. +export const getStaticPaths = localePaths; + +const { lang } = Astro.props; +--- + +<TermsPage lang={lang} /> diff --git a/src/pages/[...lang]/tools.astro b/src/pages/[...lang]/tools.astro new file mode 100644 index 0000000..acff618 --- /dev/null +++ b/src/pages/[...lang]/tools.astro @@ -0,0 +1,11 @@ +--- +import ToolsPage from '../../components/pages/ToolsPage.astro'; +import { localePaths } from '../../i18n/paths'; + +// One route per locale the scope declares; a scope that drops a locale simply does not build it. +export const getStaticPaths = localePaths; + +const { lang } = Astro.props; +--- + +<ToolsPage lang={lang} /> diff --git a/src/pages/about.astro b/src/pages/about.astro deleted file mode 100644 index d7238cc..0000000 --- a/src/pages/about.astro +++ /dev/null @@ -1,5 +0,0 @@ ---- -import AboutPage from '../components/pages/AboutPage.astro'; ---- - -<AboutPage lang="en" /> diff --git a/src/pages/builders.astro b/src/pages/builders.astro deleted file mode 100644 index 488c854..0000000 --- a/src/pages/builders.astro +++ /dev/null @@ -1,5 +0,0 @@ ---- -import BuildersPage from '../components/pages/BuildersPage.astro'; ---- - -<BuildersPage lang="en" /> diff --git a/src/pages/community-size.astro b/src/pages/community-size.astro deleted file mode 100644 index 207256e..0000000 --- a/src/pages/community-size.astro +++ /dev/null @@ -1,17 +0,0 @@ ---- -import Base from '../layouts/Base.astro'; -import CommunitySizePage from '../components/pages/CommunitySizePage.astro'; -import { nlStats } from '../data/nl-stats'; - -// The modelled figure stays in the metadata even though the page leads with the counted -// one: it is the number that gets quoted, and it should travel with its ~ attached. -const est = nlStats.estimate.modelledNl.toLocaleString('nl-NL'); ---- - -<Base - lang="en" - title="How many Dutch accounts are on AT Protocol?" - description={`AT Protocol has no country field, so we publish two numbers: the Dutch accounts we can point to, and a modelled total of around ${est}.`} -> - <CommunitySizePage lang="en" /> -</Base> diff --git a/src/pages/events.astro b/src/pages/events.astro deleted file mode 100644 index 56caf17..0000000 --- a/src/pages/events.astro +++ /dev/null @@ -1,5 +0,0 @@ ---- -import EventsPage from '../components/pages/EventsPage.astro'; ---- - -<EventsPage lang="en" /> diff --git a/src/pages/index.astro b/src/pages/index.astro deleted file mode 100644 index fca9c45..0000000 --- a/src/pages/index.astro +++ /dev/null @@ -1,5 +0,0 @@ ---- -import HomePage from '../components/pages/HomePage.astro'; ---- - -<HomePage lang="en" /> diff --git a/src/pages/login.astro b/src/pages/login.astro deleted file mode 100644 index 0e22b0b..0000000 --- a/src/pages/login.astro +++ /dev/null @@ -1,5 +0,0 @@ ---- -import LoginPage from '../components/pages/LoginPage.astro'; ---- - -<LoginPage lang="en" /> diff --git a/src/pages/members.astro b/src/pages/members.astro deleted file mode 100644 index 0a1563c..0000000 --- a/src/pages/members.astro +++ /dev/null @@ -1,5 +0,0 @@ ---- -import MembersPage from '../components/pages/MembersPage.astro'; ---- - -<MembersPage lang="en" /> diff --git a/src/pages/nl/about.astro b/src/pages/nl/about.astro deleted file mode 100644 index e41add6..0000000 --- a/src/pages/nl/about.astro +++ /dev/null @@ -1,5 +0,0 @@ ---- -import AboutPage from '../../components/pages/AboutPage.astro'; ---- - -<AboutPage lang="nl" /> diff --git a/src/pages/nl/builders.astro b/src/pages/nl/builders.astro deleted file mode 100644 index abfd6df..0000000 --- a/src/pages/nl/builders.astro +++ /dev/null @@ -1,5 +0,0 @@ ---- -import BuildersPage from '../../components/pages/BuildersPage.astro'; ---- - -<BuildersPage lang="nl" /> diff --git a/src/pages/nl/community-size.astro b/src/pages/nl/community-size.astro deleted file mode 100644 index a390351..0000000 --- a/src/pages/nl/community-size.astro +++ /dev/null @@ -1,15 +0,0 @@ ---- -import Base from '../../layouts/Base.astro'; -import CommunitySizePage from '../../components/pages/CommunitySizePage.astro'; -import { nlStats } from '../../data/nl-stats'; - -const est = nlStats.estimate.modelledNl.toLocaleString('nl-NL'); ---- - -<Base - lang="nl" - title="Hoeveel Nederlandse accounts zitten er op AT Protocol?" - description={`AT Protocol heeft geen landveld, dus publiceren we twee getallen: de Nederlandse accounts die we kunnen aanwijzen, en een gemodelleerd totaal van ongeveer ${est}.`} -> - <CommunitySizePage lang="nl" /> -</Base> diff --git a/src/pages/nl/events.astro b/src/pages/nl/events.astro deleted file mode 100644 index c741f9b..0000000 --- a/src/pages/nl/events.astro +++ /dev/null @@ -1,5 +0,0 @@ ---- -import EventsPage from '../../components/pages/EventsPage.astro'; ---- - -<EventsPage lang="nl" /> diff --git a/src/pages/nl/index.astro b/src/pages/nl/index.astro deleted file mode 100644 index 8e3daf9..0000000 --- a/src/pages/nl/index.astro +++ /dev/null @@ -1,5 +0,0 @@ ---- -import HomePage from '../../components/pages/HomePage.astro'; ---- - -<HomePage lang="nl" /> diff --git a/src/pages/nl/login.astro b/src/pages/nl/login.astro deleted file mode 100644 index ee1e80b..0000000 --- a/src/pages/nl/login.astro +++ /dev/null @@ -1,5 +0,0 @@ ---- -import LoginPage from '../../components/pages/LoginPage.astro'; ---- - -<LoginPage lang="nl" /> diff --git a/src/pages/nl/members.astro b/src/pages/nl/members.astro deleted file mode 100644 index af2472d..0000000 --- a/src/pages/nl/members.astro +++ /dev/null @@ -1,5 +0,0 @@ ---- -import MembersPage from '../../components/pages/MembersPage.astro'; ---- - -<MembersPage lang="nl" /> diff --git a/src/pages/nl/privacy.astro b/src/pages/nl/privacy.astro deleted file mode 100644 index db9ed19..0000000 --- a/src/pages/nl/privacy.astro +++ /dev/null @@ -1,5 +0,0 @@ ---- -import PrivacyPage from '../../components/pages/PrivacyPage.astro'; ---- - -<PrivacyPage lang="nl" /> diff --git a/src/pages/nl/terms.astro b/src/pages/nl/terms.astro deleted file mode 100644 index 029705c..0000000 --- a/src/pages/nl/terms.astro +++ /dev/null @@ -1,5 +0,0 @@ ---- -import TermsPage from '../../components/pages/TermsPage.astro'; ---- - -<TermsPage lang="nl" /> diff --git a/src/pages/nl/tools.astro b/src/pages/nl/tools.astro deleted file mode 100644 index 3c097cc..0000000 --- a/src/pages/nl/tools.astro +++ /dev/null @@ -1,5 +0,0 @@ ---- -import ToolsPage from '../../components/pages/ToolsPage.astro'; ---- - -<ToolsPage lang="nl" /> diff --git a/src/pages/privacy.astro b/src/pages/privacy.astro deleted file mode 100644 index d36fb2d..0000000 --- a/src/pages/privacy.astro +++ /dev/null @@ -1,5 +0,0 @@ ---- -import PrivacyPage from '../components/pages/PrivacyPage.astro'; ---- - -<PrivacyPage lang="en" /> diff --git a/src/pages/terms.astro b/src/pages/terms.astro deleted file mode 100644 index 476ded6..0000000 --- a/src/pages/terms.astro +++ /dev/null @@ -1,5 +0,0 @@ ---- -import TermsPage from '../components/pages/TermsPage.astro'; ---- - -<TermsPage lang="en" /> diff --git a/src/pages/tools.astro b/src/pages/tools.astro deleted file mode 100644 index 8893445..0000000 --- a/src/pages/tools.astro +++ /dev/null @@ -1,5 +0,0 @@ ---- -import ToolsPage from '../components/pages/ToolsPage.astro'; ---- - -<ToolsPage lang="en" />