diff --git a/src/app-state.ts b/src/app-state.ts
index 8b0beb3..d3de814 100644
--- a/src/app-state.ts
+++ b/src/app-state.ts
@@ -60,10 +60,14 @@ export function goToToday(): void {
setDate(todayIso());
}
-export function shiftDate(days: number): void {
- const d = new Date(`${state.date}T00:00:00Z`);
+export function addDays(date: string, days: number): string {
+ const d = new Date(`${date}T00:00:00Z`);
d.setUTCDate(d.getUTCDate() + days);
- setDate(d.toISOString().slice(0, 10));
+ return d.toISOString().slice(0, 10);
+}
+
+export function shiftDate(days: number): void {
+ setDate(addDays(state.date, days));
}
export function setSelectedHour(hourId: HourId): void {
diff --git a/src/router.ts b/src/router.ts
index 9d96434..b8c8046 100644
--- a/src/router.ts
+++ b/src/router.ts
@@ -1,4 +1,4 @@
-import { getState, setDate, setSelectedHour, subscribe } from './app-state';
+import { getState, setDate, setSelectedHour, setLanguages, subscribe, type AppState } from './app-state';
import { HOUR_IDS, type HourId } from './hours/types';
// Real paths (reground.org/vu/2026-08-09/prime), not hash routing — the
@@ -10,37 +10,55 @@ function isHourId(value: string): value is HourId {
return (HOUR_IDS as readonly string[]).includes(value);
}
-function parseLocation(): { date?: string; hour?: HourId } {
+function parseLanguages(value: string | null): AppState['languages'] | undefined {
+ if (!value) return undefined;
+ const langs = value.split(',').filter(Boolean);
+ if (langs.length === 1) return [langs[0]!];
+ if (langs.length === 2) return [langs[0]!, langs[1]!];
+ return undefined;
+}
+
+function parseLocation(): { date?: string; hour?: HourId; languages?: AppState['languages'] } {
const path = window.location.pathname;
if (!path.startsWith(BASE)) return {};
const [date, hour] = path.slice(BASE.length).split('/').filter(Boolean);
+ const languages = parseLanguages(new URLSearchParams(window.location.search).get('lang'));
return {
date: date && /^\d{4}-\d{2}-\d{2}$/.test(date) ? date : undefined,
hour: hour && isHourId(hour) ? hour : undefined,
+ languages,
};
}
-function urlFor(date: string, hour: HourId): string {
- return `${BASE}${date}/${hour}`;
+/** Exported for reuse by day-nav/bilingual-toggle so they can render real
+ * `` links (right-click / open-in-new-tab / copy-link) instead of
+ * reimplementing this path-building logic. */
+export function urlFor(date: string, hour: HourId, languages: AppState['languages']): string {
+ const base = `${BASE}${date}/${hour}`;
+ // Omit the query string for the default single-English case to keep the
+ // common-case URL clean.
+ if (languages.length === 1 && languages[0] === 'en') return base;
+ return `${base}?lang=${languages.join(',')}`;
}
let lastUrl = '';
function pushUrl(): void {
- const { date, selectedHour } = getState();
- const url = urlFor(date, selectedHour);
+ const { date, selectedHour, languages } = getState();
+ const url = urlFor(date, selectedHour, languages);
if (url === lastUrl) return;
lastUrl = url;
window.history.pushState(null, '', url);
}
export function initRouter(): void {
- const { date, hour } = parseLocation();
+ const { date, hour, languages } = parseLocation();
if (date) setDate(date);
if (hour) setSelectedHour(hour);
+ if (languages) setLanguages(languages);
- const { date: currentDate, selectedHour } = getState();
- lastUrl = urlFor(currentDate, selectedHour);
+ const { date: currentDate, selectedHour, languages: currentLanguages } = getState();
+ lastUrl = urlFor(currentDate, selectedHour, currentLanguages);
window.history.replaceState(null, '', lastUrl);
subscribe(() => pushUrl());
@@ -49,8 +67,9 @@ export function initRouter(): void {
const parsed = parseLocation();
// Set lastUrl first so the notify() inside setDate/setSelectedHour below
// doesn't turn this back-navigation into a new forward pushState.
- lastUrl = window.location.pathname;
+ lastUrl = window.location.pathname + window.location.search;
if (parsed.date) setDate(parsed.date);
if (parsed.hour) setSelectedHour(parsed.hour);
+ if (parsed.languages) setLanguages(parsed.languages);
});
}
diff --git a/src/ui/bilingual-toggle.ts b/src/ui/bilingual-toggle.ts
index 548bc1d..cc1fd54 100644
--- a/src/ui/bilingual-toggle.ts
+++ b/src/ui/bilingual-toggle.ts
@@ -1,4 +1,5 @@
import { getState, setLanguages, type AppState } from '../app-state';
+import { urlFor } from '../router';
// Hardcoded to en/la since those are the only languages with any data so
// far. The two-language ceiling (never three+ columns) is the real
@@ -11,21 +12,28 @@ const OPTIONS: { label: string; languages: AppState['languages'] }[] = [
];
export function renderLanguageToggle(container: HTMLElement): void {
- const { languages } = getState();
+ const { date, selectedHour, languages } = getState();
const current = languages.join('+');
container.innerHTML = `