Add bilingual day-label headers with Latin weekday/season/ordinal vocabulary

Builds every day label as a {en, la} pair (weekdays, ranks, season/ordinal
phrasing authored in Latin; proper names without an authored Latin form
fall back to their English string) and merges the date+day-label into one
header in the day-nav bar.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 05:27:44 -04:00
parent ef71d9c5bc
commit e7bbc7c9f5
12 changed files with 415 additions and 143 deletions
+18 -3
View File
@@ -1,13 +1,28 @@
import { getState, shiftDate, goToToday } from '../app-state';
import { formatDateLong } from './format';
import { resolveDay } from '../calendar';
import { getDayLabel } from '../calendar/day-label';
import { formatDateLong, renderBilingual } from './format';
/** Renders navigation plus the merged "when" header: the civil date/
* weekday and the liturgical day-being-celebrated, together in one place
* — previously the date lived here while the day-being-celebrated text
* was repeated separately inside every hour's own view. The day-being-
* celebrated text is hour-independent (the same LiturgicalDay for every
* hour on a given date), so it's resolved directly here via `resolveDay`
* rather than through any particular hour's `resolveOrdo`. */
export function renderDayNav(container: HTMLElement): void {
const { date } = getState();
const { date, languages } = getState();
const day = resolveDay(date);
const dateLabel = formatDateLong(date);
const dayLabel = getDayLabel(day);
container.innerHTML = `
<nav class="day-nav" aria-label="Day navigation">
<button type="button" class="day-nav-btn" data-action="prev" aria-label="Previous day">&larr;</button>
<button type="button" class="day-nav-btn day-nav-today" data-action="today">Today</button>
<span class="day-nav-date">${formatDateLong(date)}</span>
<div class="day-nav-header">
${renderBilingual(dateLabel, languages, 'day-nav-date')}
${renderBilingual(dayLabel, languages, 'day-nav-label')}
</div>
<button type="button" class="day-nav-btn" data-action="next" aria-label="Next day">&rarr;</button>
</nav>
`;
+50 -5
View File
@@ -1,17 +1,62 @@
/** Formats an ISO date as e.g. "Sunday, August 9, 2026" — always in UTC so
* it matches calendar/weekday.ts's interpretation regardless of the
* viewer's local timezone. */
export function formatDateLong(isoDate: string): string {
// Latin month names have no Intl locale data, so the genitive forms below
// (used the same way English "of August" would be, e.g. "18 Augusti 2026")
// are authored directly here.
const MONTH_LABELS_LA = [
'Ianuarii',
'Februarii',
'Martii',
'Aprilis',
'Maii',
'Iunii',
'Iulii',
'Augusti',
'Septembris',
'Octobris',
'Novembris',
'Decembris',
];
const WEEKDAY_LABELS_LA = ['Dominica', 'Feria Secunda', 'Feria Tertia', 'Feria Quarta', 'Feria Quinta', 'Feria Sexta', 'Sabbato'];
/** Formats an ISO date as e.g. "Sunday, August 9, 2026" (`en`) / "Dominica,
* die 9 Augusti 2026" (`la`) — always in UTC so it matches
* calendar/weekday.ts's interpretation regardless of the viewer's local
* timezone. Weekday naming mirrors calendar/day-label.ts's own
* `WEEKDAY_LABELS` (spelled-out Latin ferial names, not roman numerals). */
export function formatDateLong(isoDate: string): Partial<Record<string, string>> {
const date = new Date(`${isoDate}T00:00:00Z`);
return new Intl.DateTimeFormat('en-US', {
const en = new Intl.DateTimeFormat('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
timeZone: 'UTC',
}).format(date);
const day = date.getUTCDate();
const month = MONTH_LABELS_LA[date.getUTCMonth()];
const year = date.getUTCFullYear();
const weekday = WEEKDAY_LABELS_LA[date.getUTCDay()];
const la = `${weekday}, die ${day} ${month} ${year}`;
return { en, la };
}
export function hourLabel(hourId: string): string {
return hourId.charAt(0).toUpperCase() + hourId.slice(1);
}
function escapeHtml(value: string): string {
return value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
/** Renders one `<div class="lang-column">` per currently-active language
* for a bilingual text map — the same pattern hour-view.ts's own
* `renderColumns` uses for resolved liturgical text, shared here so
* day-nav.ts's header can reuse it instead of re-implementing it. */
export function renderBilingual(text: Partial<Record<string, string>>, languages: readonly string[], className: string): string {
const cols = languages
.map((lang) => `<div class="lang-column" lang="${lang}">${escapeHtml(text[lang] ?? '')}</div>`)
.join('');
return `<div class="${className} lang-columns lang-columns-${languages.length}">${cols}</div>`;
}
-1
View File
@@ -149,7 +149,6 @@ export function renderHourView(container: HTMLElement): void {
container.innerHTML = `
<div class="hour-view">
<h2>${hourLabel(selectedHour)}</h2>
${ordo.dayLabel ? `<p class="day-label">${escapeHtml(ordo.dayLabel)}</p>` : ''}
${ordo.parts.map((part) => renderPart(part, languages)).join('')}
</div>
`;
+16 -10
View File
@@ -165,12 +165,26 @@ button:focus-visible {
cursor: pointer;
}
.day-nav-date {
font-weight: 600;
.day-nav-header {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.15rem;
min-width: 16rem;
text-align: center;
}
.day-nav-date {
font-weight: 600;
}
.day-nav-label {
font-family: var(--font-ui);
font-size: 0.85rem;
font-style: italic;
color: var(--color-brass);
}
/* --------------------------------------------------------------------------
Layout: hour list + hour view
-------------------------------------------------------------------------- */
@@ -261,14 +275,6 @@ button:focus-visible {
margin: 0 0 var(--space-1);
}
.day-label {
font-family: var(--font-ui);
font-size: 0.9em;
font-style: italic;
color: var(--color-brass);
margin: 0 0 var(--space-2);
}
.ordo-part-citation {
font-family: var(--font-ui);
font-size: 0.85em;