Skip to content

Commit 90e2e72

Browse files
committed
feat: implement internationalization support and enhance site components
- Added i18n configuration to support multiple locales (en, es, pt, it, fr, de, nl) in astro.config.mjs. - Created i18n.ts to manage locale data, including labels and codes. - Updated BaseHead.astro to generate alternate URLs for different locales and improve Open Graph metadata. - Refactored ActionLink.astro, CapabilityIndex.astro, DownloadPanel.astro, and other components to utilize localized content and improve accessibility. - Enhanced SiteNav.astro and SiteFooter.astro to reflect localized navigation and footer links. - Improved community.astro to dynamically display community information based on the selected locale.
1 parent 9737817 commit 90e2e72

24 files changed

Lines changed: 3220 additions & 616 deletions

astro.config.mjs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@ import tailwindcss from '@tailwindcss/vite';
66
export default defineConfig({
77
site: 'https://amonite.org',
88
output: 'static',
9+
i18n: {
10+
defaultLocale: 'en',
11+
locales: ['en', 'es', 'pt', 'it', 'fr', 'de', 'nl'],
12+
routing: {
13+
prefixDefaultLocale: false,
14+
},
15+
},
916
integrations: [sitemap()],
1017
vite: {
1118
plugins: [tailwindcss()],

docs/VISUAL.md

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,24 @@ Stylised ammonite. Official assets only (`src/assets/logo.png`, `public/` favico
1515

1616
## Colour
1717

18-
Neutrals carry the UI. Amber is a sparse accent.
19-
20-
| Token | Value | Role |
21-
| --------------- | --------- | --------------------------- |
22-
| amber / primary | `#C08A2B` | Accent, links |
23-
| stone | `#8B8B4A` | Rare earth accent |
24-
| sand | `#DBC3A5` | Soft panels |
25-
| mist | `#ECEBE1` | Default background |
26-
| slate | `#4A4F57` | Muted text, borders |
27-
| graphite | `#34373D` | Primary text; dark surfaces |
28-
| deep-green | `#3D5A40` | Occasional natural accent |
18+
Neutrals carry the UI. Amber is a sparse accent. Components use semantic
19+
tokens so the same hierarchy remains legible in light and dark appearances.
20+
21+
| Token | Light value | Dark value | Role |
22+
| ----------------- | ----------- | ---------- | ---------------------------- |
23+
| surface | `#ECEBE1` | `#1B1E1F` | Page background |
24+
| surface-soft | `#E8E3D7` | `#25292A` | Quiet panels and footer |
25+
| surface-inverse | `#34373D` | `#2B2F30` | Hero and emphasis surfaces |
26+
| text | `#34373D` | `#ECE9DF` | Primary text |
27+
| text-muted | `#4A4F57` | `#B8BCB8` | Supporting text |
28+
| accent | `#805C17` | `#E6B85A` | Accent text and markers |
29+
| accent-surface | `#805C17` | `#805C17` | Primary accent controls |
2930

3031
No saturated blues, purples, neon, or glow chrome.
3132

33+
The appearance selector offers System, Light, and Dark. System follows the
34+
operating system preference and is the default.
35+
3236
## Typography
3337

3438
| Role | Family |
@@ -58,4 +62,6 @@ Cyberpunk / RGB spectacle · SaaS landing theatre · fear-based security cliché
5862

5963
## Accessibility
6064

61-
Readable graphite on mist. Visible focus rings. Honour `prefers-reduced-motion`.
65+
Semantic contrast pairs are selected for readability in both appearances.
66+
Focus rings remain visible on light and dark surfaces. Honour
67+
`prefers-reduced-motion`.

src/components/BaseHead.astro

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,38 @@
11
---
22
import { getImage } from 'astro:assets';
33
import type { ImageMetadata } from 'astro';
4+
import { getAbsoluteLocaleUrl } from 'astro:i18n';
45
import FallbackImage from '../assets/screenshots/first-boot.png';
56
import { SITE_AUTHOR, SITE_TITLE } from '../consts';
7+
import { LOCALE_CODES, LOCALES, getLocale, MEDIA_COPY, type Locale } from '../i18n';
68
79
interface Props {
810
title: string;
911
description: string;
1012
image?: ImageMetadata | string;
1113
/** Describes the share image for screen readers and AI extraction. */
1214
imageAlt?: string;
15+
locale?: Locale;
1316
}
1417
1518
/** Matches FallbackImage. Pages that pass their own image should pass their own alt. */
16-
const FALLBACK_IMAGE_ALT =
17-
'Amonite live session at first boot, with the Welcome handbook and the installer open on an otherwise empty desktop';
18-
19-
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
20-
2119
const {
2220
title,
2321
description,
2422
image = FallbackImage,
25-
imageAlt = FALLBACK_IMAGE_ALT,
23+
imageAlt: imageAltProp,
24+
locale: localeProp,
2625
} = Astro.props;
26+
const locale = getLocale(localeProp ?? Astro.currentLocale);
27+
const imageAlt = imageAltProp ?? MEDIA_COPY[locale].fallbackImageAlt;
28+
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
29+
const routeSegments = Astro.url.pathname.split('/').filter(Boolean);
30+
if (routeSegments[0] === locale) routeSegments.shift();
31+
const localizedPath = routeSegments.join('/');
32+
const alternateUrls = LOCALES.map((alternateLocale) => ({
33+
locale: alternateLocale,
34+
href: getAbsoluteLocaleUrl(alternateLocale, localizedPath),
35+
}));
2736
2837
const ogImage =
2938
typeof image === 'string'
@@ -44,8 +53,12 @@ const imageURL = new URL(ogImage.src, Astro.site);
4453
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
4554
<meta name="generator" content={Astro.generator} />
4655
<meta name="author" content={SITE_AUTHOR} />
47-
<meta name="color-scheme" content="light" />
56+
<meta name="color-scheme" content="light dark" />
4857
<link rel="canonical" href={canonicalURL} />
58+
{alternateUrls.map((alternate) => (
59+
<link rel="alternate" hreflang={alternate.locale} href={alternate.href} />
60+
))}
61+
<link rel="alternate" hreflang="x-default" href={alternateUrls[0].href} />
4962
<meta
5063
name="robots"
5164
content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1"
@@ -67,7 +80,14 @@ const imageURL = new URL(ogImage.src, Astro.site);
6780
{ogImage.width && <meta property="og:image:width" content={String(ogImage.width)} />}
6881
{ogImage.height && <meta property="og:image:height" content={String(ogImage.height)} />}
6982
<meta property="og:site_name" content={SITE_TITLE} />
70-
<meta property="og:locale" content="en_US" />
83+
<meta property="og:locale" content={LOCALE_CODES[locale].replace('-', '_')} />
84+
{
85+
alternateUrls
86+
.filter((alternate) => alternate.locale !== locale)
87+
.map((alternate) => (
88+
<meta property="og:locale:alternate" content={LOCALE_CODES[alternate.locale].replace('-', '_')} />
89+
))
90+
}
7191

7292
<meta name="twitter:card" content="summary_large_image" />
7393
<meta name="twitter:title" content={title} />

src/components/site/ActionLink.astro

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
---
2+
import { getLocale, SHARED_COPY } from '../../i18n';
3+
24
export interface Props {
35
href: string;
46
variant?: 'primary' | 'secondary' | 'accent' | 'ghost';
@@ -12,18 +14,20 @@ const {
1214
class: className = '',
1315
external = false,
1416
} = Astro.props;
17+
const copy = SHARED_COPY[getLocale(Astro.currentLocale)];
1518
1619
const variants = {
17-
primary: 'bg-graphite text-mist hover:bg-graphite/90',
18-
secondary: 'border border-border bg-mist text-graphite hover:border-slate/40',
19-
accent: 'bg-amber text-graphite hover:bg-sand',
20-
ghost: 'text-graphite underline decoration-border underline-offset-[5px] hover:decoration-slate',
20+
primary: 'bg-surface-inverse text-text-inverse hover:bg-surface-inverse/90',
21+
secondary: 'border border-border bg-surface text-text hover:border-text-muted/40',
22+
accent:
23+
'bg-accent-surface text-accent-foreground hover:bg-accent-hover-surface hover:text-text',
24+
ghost: 'text-text underline decoration-border underline-offset-[5px] hover:decoration-text-muted',
2125
};
2226
2327
const base =
2428
variant === 'ghost'
25-
? 'inline font-medium'
26-
: 'inline-flex items-center justify-center rounded-default px-5 py-2.5 text-sm font-medium transition-colors';
29+
? 'inline-flex min-h-11 items-center font-medium transition-colors'
30+
: 'inline-flex min-h-11 items-center justify-center rounded-default px-5 py-2.5 text-sm font-medium transition-colors';
2731
---
2832

2933
<a
@@ -32,4 +36,5 @@ const base =
3236
{...(external ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
3337
>
3438
<slot />
39+
{external && <span class="sr-only"> {copy.opensNewTab}</span>}
3540
</a>

src/components/site/CapabilityIndex.astro

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,32 +4,36 @@
44
* `via` lines expose how each capability is realized; they stay
55
* subordinate to the outcome, never above it.
66
*/
7-
import { DISTRIBUTION } from '../../consts';
7+
import { getCopy, getLocale, SHARED_COPY, type Locale } from '../../i18n';
88
99
export interface Props {
1010
showVia?: boolean;
1111
class?: string;
12+
locale?: Locale;
1213
}
1314
14-
const { showVia = false, class: className = '' } = Astro.props;
15+
const { showVia = false, class: className = '', locale: localeProp } = Astro.props;
16+
const locale = getLocale(localeProp ?? Astro.currentLocale);
17+
const copy = getCopy(locale);
18+
const shared = SHARED_COPY[locale];
1519
---
1620

1721
<ol class:list={['border-t border-border', className]}>
1822
{
19-
DISTRIBUTION.capabilities.map((item, i) => (
23+
copy.product.capabilities.map((item, i) => (
2024
<li class="grid gap-x-8 gap-y-2 border-b border-border py-5 sm:grid-cols-[3.5rem_15rem_1fr] sm:py-6 lg:grid-cols-[4rem_16rem_1fr]">
21-
<span class="font-mono text-sm text-slate/60" aria-hidden="true">
25+
<span class="font-mono text-sm text-text-muted/60" aria-hidden="true">
2226
{String(i + 1).padStart(2, '0')}
2327
</span>
24-
<h3 class="text-base font-semibold tracking-tight text-graphite">
28+
<h3 class="text-base font-semibold tracking-tight text-text">
2529
{item.title}
2630
</h3>
27-
<div class="text-sm leading-relaxed text-slate sm:col-start-3">
31+
<div class="text-sm leading-relaxed text-text-muted sm:col-start-3">
2832
<p>{item.body}</p>
2933
{showVia && (
30-
<p class="mt-3 border-l-2 border-sand pl-3 text-[0.8125rem] leading-relaxed text-slate/90">
31-
<span class="font-mono text-xs uppercase tracking-[0.08em] text-amber">
32-
via{' '}
34+
<p class="mt-3 border-l-2 border-accent-soft pl-3 text-[0.8125rem] leading-relaxed text-text-muted/90">
35+
<span class="font-mono text-xs uppercase tracking-[0.08em] text-accent">
36+
{shared.via}{' '}
3337
</span>
3438
{item.via}
3539
</p>

src/components/site/DownloadPanel.astro

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,48 +4,68 @@
44
*/
55
import ActionLink from './ActionLink.astro';
66
import { DISTRIBUTION, PUBLIC_EDITIONS } from '../../consts';
7+
import {
8+
formatCopy,
9+
getCopy,
10+
getLocale,
11+
getReleaseCopy,
12+
localizedPath,
13+
SHARED_COPY,
14+
type Locale,
15+
} from '../../i18n';
716
17+
export interface Props {
18+
locale?: Locale;
19+
}
20+
21+
const { locale: localeProp } = Astro.props;
22+
const locale = getLocale(localeProp ?? Astro.currentLocale);
23+
const copy = getCopy(locale);
24+
const shared = SHARED_COPY[locale];
25+
const releaseCopy = getReleaseCopy(locale, DISTRIBUTION.editionId, DISTRIBUTION.release);
826
const availableEditions = PUBLIC_EDITIONS.filter(
927
(edition) => edition.status === 'available',
1028
);
1129
const availableLabel =
1230
availableEditions.length > 0
13-
? `${availableEditions.map((edition) => edition.name).join(' and ')} available`
14-
: 'Editions';
31+
? formatCopy(copy.home.available, {
32+
editions: availableEditions.map((edition) => edition.name).join(' · '),
33+
})
34+
: copy.home.editionsEyebrow;
1535
---
1636

17-
<aside class="border border-border bg-graphite text-mist">
37+
<aside class="border border-border bg-surface-inverse text-text-inverse">
1838
<div class="grid gap-8 p-8 sm:p-10 lg:grid-cols-[1.25fr_1fr] lg:items-end lg:gap-12">
1939
<div>
20-
<p class="label-mono font-medium text-sand">
21-
{DISTRIBUTION.editionName} · {DISTRIBUTION.releaseLabel}
40+
<p class="label-mono font-medium text-accent-soft">
41+
{DISTRIBUTION.editionName} · {releaseCopy.label}
2242
</p>
2343
<h2 class="mt-4 text-2xl font-semibold tracking-tight sm:text-3xl">
24-
Get the operating system
44+
{copy.nav.downloads}
2545
</h2>
26-
<p class="mt-3 max-w-xl text-sm leading-relaxed text-mist/75 sm:text-[0.95rem] sm:leading-relaxed">
27-
{`${availableLabel}. Download and verify current releases.`}
46+
<p class="mt-3 max-w-xl text-sm leading-relaxed text-text-inverse/75 sm:text-[0.95rem] sm:leading-relaxed">
47+
{`${availableLabel}. ${copy.home.downloadVerify}.`}
2848
</p>
29-
<p class="mt-5 font-mono text-xs text-mist/50">
30-
{DISTRIBUTION.isoFileName} · SHA256 · GPG signed
49+
<p class="mt-5 font-mono text-xs text-text-inverse/75">
50+
{DISTRIBUTION.isoFileName} · SHA256 · {shared.signed}
3151
</p>
3252
</div>
3353

3454
<div class="flex flex-col gap-3 sm:items-stretch lg:items-end">
3555
<ActionLink
36-
href="/downloads"
56+
href={localizedPath(locale, 'downloads')}
3757
variant="accent"
3858
class="w-full justify-center sm:w-auto"
3959
>
40-
Downloads & verification
60+
{copy.common.downloads} & {copy.common.verificationGuide}
4161
</ActionLink>
4262
<ActionLink
4363
href={DISTRIBUTION.artifacts.releaseNotes.href}
4464
variant="ghost"
4565
external
46-
class="text-center text-mist/80 decoration-mist/30 hover:text-mist hover:decoration-mist/60 sm:text-right"
66+
class="text-center text-text-inverse/90 decoration-text-inverse/60 hover:text-text-inverse hover:decoration-text-inverse sm:text-right"
4767
>
48-
{`Open ${DISTRIBUTION.editionName} release`}
68+
{`${copy.common.openRelease} · ${DISTRIBUTION.editionName}`}
4969
</ActionLink>
5070
</div>
5171
</div>

src/components/site/EditionOverview.astro

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,39 +5,53 @@
55
* Renders every public edition from data — does not assume how many exist.
66
*/
77
import { PUBLIC_EDITIONS } from '../../consts';
8+
import { getCopy, getEditionCopy, getLocale, type Locale } from '../../i18n';
89
910
export interface Props {
1011
class?: string;
12+
locale?: Locale;
1113
}
1214
13-
const { class: className = '' } = Astro.props;
15+
const { class: className = '', locale: localeProp } = Astro.props;
16+
const locale = getLocale(localeProp ?? Astro.currentLocale);
17+
const copy = getCopy(locale);
1418
---
1519

1620
<ol class:list={['border-t border-border', className]}>
1721
{
18-
PUBLIC_EDITIONS.map((edition, i) => (
19-
<li class="grid gap-x-8 gap-y-3 border-b border-border py-6 sm:grid-cols-[3.5rem_10rem_1fr] sm:py-7 lg:grid-cols-[4rem_12rem_1fr]">
20-
<span class="font-mono text-sm text-slate/60" aria-hidden="true">
22+
PUBLIC_EDITIONS.map((edition, i) => {
23+
const editionCopy = getEditionCopy(locale, edition.id);
24+
const statusLabel =
25+
edition.status === 'available'
26+
? copy.common.available
27+
: edition.status === 'experimental'
28+
? copy.common.experimental
29+
: edition.statusLabel;
30+
31+
return (
32+
<li class="grid gap-x-8 gap-y-3 border-b border-border py-6 sm:grid-cols-[3.5rem_10rem_1fr] sm:py-7 lg:grid-cols-[4rem_12rem_1fr]">
33+
<span class="font-mono text-sm text-text-muted/60" aria-hidden="true">
2134
{String(i + 1).padStart(2, '0')}
2235
</span>
2336
<div>
24-
<h3 class="text-base font-semibold tracking-tight text-graphite">
37+
<h3 class="text-base font-semibold tracking-tight text-text">
2538
{edition.name}
2639
</h3>
27-
<p class="label-mono mt-1 text-amber">{edition.statusLabel}</p>
40+
<p class="label-mono mt-1 text-accent">{statusLabel}</p>
2841
</div>
29-
<div class="space-y-3 text-sm leading-relaxed text-slate sm:col-start-3">
30-
{edition.audience && <p>{edition.audience}</p>}
31-
{edition.difference && <p>{edition.difference}</p>}
42+
<div class="space-y-3 text-sm leading-relaxed text-text-muted sm:col-start-3">
43+
{editionCopy?.audience && <p>{editionCopy.audience}</p>}
44+
{editionCopy?.difference && <p>{editionCopy.difference}</p>}
3245
{edition.role && <p>{edition.role}</p>}
33-
<p>{edition.statusDetail}</p>
46+
<p>{editionCopy?.statusDetail ?? edition.statusDetail}</p>
3447
{edition.currentRelease && (
35-
<p class="font-mono text-xs text-slate/80">
36-
Current release · {edition.currentRelease.version}
48+
<p class="font-mono text-xs text-text-muted">
49+
{copy.common.currentRelease} · {edition.currentRelease.version}
3750
</p>
3851
)}
3952
</div>
4053
</li>
41-
))
54+
);
55+
})
4256
}
4357
</ol>

src/components/site/PageHeader.astro

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,19 @@ export interface Props {
88
const { title, lead, eyebrow } = Astro.props;
99
---
1010

11-
<header class="border-b border-border bg-sand/15">
11+
<header class="border-b border-border bg-surface-soft">
1212
<div class="mx-auto max-w-6xl px-6 py-16 sm:py-20">
1313
{
1414
eyebrow && (
15-
<p class="label-mono font-medium text-amber">{eyebrow}</p>
15+
<p class="label-mono font-medium text-accent">{eyebrow}</p>
1616
)
1717
}
18-
<h1 class="display-serif mt-3 max-w-3xl text-3xl leading-tight text-graphite sm:text-4xl md:text-[2.75rem]">
18+
<h1 class="display-serif mt-3 max-w-3xl text-3xl leading-tight text-text sm:text-4xl md:text-[2.75rem]">
1919
{title}
2020
</h1>
2121
{
2222
lead && (
23-
<p class="mt-5 max-w-2xl text-lg leading-relaxed text-slate">{lead}</p>
23+
<p class="mt-5 max-w-2xl text-lg leading-relaxed text-text-muted">{lead}</p>
2424
)
2525
}
2626
</div>

0 commit comments

Comments
 (0)