Reference

Liturgy

The liturgical data domain and how Gianna's public /liturgy/v1 API represents it.

liturgyapireference

Updated 2026-06-17 · https://gianna.chat/liturgy/v1

This is a large and complex domain. These notes must stay concise to be useful.

The liturgical data domain and how this API represents it. A domain model, not a record of what is built. Notes flag where a shape is a deliberate choice or limit; Boundaries list what we don't serve.

What the API provides

For a (calendar, region, date), resolves and returns:

  1. Day context — civil date + weekday, season (+ week), liturgical-year cycles (OF lectionary A/B/C, weekday I/II, psalter week), obligation status.
  2. Celebration — what the day is: name (multilingual), rank, colour, saint slug, Common categories, octave membership.
  3. Commemorations — other saints folded in as added prayers.
  4. Feasts today — every saint bearing on this date, tagged by outcome (celebrated / commemorated / omitted / arrived), so "whose feast is it?" needs no resolution parsing (§6).
  5. Mass(es)propers (Introit … Postcommunion) + readings (citation + text); plural where a day has several (Christmas, vigils, Requiem).
  6. Office — the eight hours assembled (psalms, antiphons, hymn, chapter, canticle, collect), first/second Vespers distinguished.
  7. Resolution (optional) — why this office won: who was commemorated/omitted/ transferred/not-observed.
  8. Saints — addressable per rite (/{calendar}/saints/{id}).

All human-readable text is a { la, en, … } map, Latin the floor. The liturgy is a resolution: the temporale (seasons, from Easter) and sanctorale (fixed feasts) each propose an office; precedence (occurrence within a day, concurrence at its edges) picks the winner and the fate of the rest; the winner supplies Mass + Office, assembled Proper → Common → Psalter.

Endpoints

Placement rule. Cross-rite resources are top-level (/calendars, /saints, /regions); rite-scoped resources nest under /calendars/{cal}. A day returns a cheap summary; heavy layers come either inline via ?include= (the batch case) or as addressable sub-resources (the deep-link case) — both exist (see Includes).

methodpathqueryreturns
GET/liturgy/v1service index — links to the collections below + openapi.json
GET/liturgy/v1/openapi.jsonthe OpenAPI 3.1 contract (source of truth for every shape)
GET/liturgy/v1/calendarsthe rites — ids (ordinary/traditional/dominican), edition, hours exposed
GET/liturgy/v1/calendars/{cal}one rite's metadata
GET/liturgy/v1/calendars/{cal}/days/{date}region, include, translationssummary (below)
GET/liturgy/v1/calendars/{cal}/daysfrom,to,region,include,translationsrange of summaries (collection; capped span)
GET/liturgy/v1/calendars/{cal}/years/{label}region,include,translationsone liturgical year of summaries (collection); canonical {label} = YYYY-YYYY (the Advent-to-Advent span)
GET/liturgy/v1/calendars/{cal}/days/{date}/officeregion,translationsindex of the hours this day exposes
GET/liturgy/v1/calendars/{cal}/days/{date}/office/{hour}region,translationsone hour (lauds…; first-vespers/second-vespers; bare vespers ⇒ second)
GET/liturgy/v1/calendars/{cal}/days/{date}/massesregion,translationsfull Mass propers (ordered readings + chants, with text)
GET/liturgy/v1/calendars/{cal}/days/{date}/masses/{mass}region,translationsone Mass (midnight/dawn/day…)
GET/liturgy/v1/calendars/{cal}/days/{date}/resolutionregionwhy this office won: winner, displaced, transfer chains (§4)
GET/liturgy/v1/saintsq,title,month,pagesearch across all rites (collection, §11)
GET/liturgy/v1/saints/{person}one saint, all calendars at once (§11)
GET/liturgy/v1/calendars/{cal}/saintsq,title,month,pagesearch within one rite (collection, §11)
GET/liturgy/v1/calendars/{cal}/saints/{id}one saint's rite-scoped entry (§3)
GET/liturgy/v1/regionsknown regions + what each overrides (transfer/precept)
GET/liturgy/v1/translationsavailable scripture editions per language + each language's default
GET/liturgy/v1/novenasyearthe novena catalogue with each one's computed feast + suggested start (no prayer text)
GET/liturgy/v1/novenas/upcomingfrom,daysnovenas whose traditional start falls within the window, soonest first
GET/liturgy/v1/novenas/{slug}yearone novena, with day-by-day prayer text when available

Includes. ?include=masses,office,resolution,martyrology folds those heavy layers inline into a day (or every day of a range/year), so the dominant "month of readings" view is one call, not 1 + N. The sub-resource URLs (…/masses, …/office/{hour}, …/resolution) remain for addressable deep-links. Without include, a day is just the summary + links.

Collections (/days?from..to, /years/{label}, both /saints searches) return an envelope, never a bare array — so the echoed query, totals, paging, and any applied caps surface:

{ "results": [ … ], "query": { … echoed, incl. region }, "page": { "total": 342, "next": "…cursor…" },
  "warnings": [ "span capped to 90 days" ] }   // warnings only when something was clamped

(Arrays inside one resource — saints.*, readings — are bounded sub-fields, not collections; they stay bare.)

Identifiers & negotiation.

Caching. A response is a pure function of (cal, date, region, Accept-Language, translations, data-version). The ETag incorporates all six — not just the path — so a CDN keyed on path+Vary can't mis-serve a region/language/translation variant, and a data correction or newly-added region table bumps data-version, changing every affected ETag (the invalidation handle for past days, which are cached long but never immutable). If-None-Match304.

Versioning & lifecycle. /liturgy/v1 is additive within the major version: fields may be added (the Open-questions shapes will), never removed or retyped. New editions/regions arrive as data (§2), not new versions. A breaking change ships as /liturgy/v2; the old version is then dated for removal via Deprecation + Sunset response headers (≥ 12 months' notice), and deprecated fields carry a Deprecation link in the docs. Nothing in v1 is ever silently changed.

Errors are application/problem+json (RFC 9457) with a stable machine type URI, title, status, detail, and instance:

Conventions.


1. Calendars (rites)

A self-contained system — own sanctorale, precedence, propers, edition. A date resolves independently in each, so precedence is per-rite (§4), not global.

idriterubrics
ordinaryGeneral Roman Calendar, Ordinary Form (post-1969)1969 General Norms
traditionalTraditional Roman (Extraordinary Form / "TLM")1960 Code of Rubrics
dominicanDominican Rite (OP)OP 1962

Each has one stable id — ordinary, traditional, dominican; no aliases — and advertises whether it exposes an Office.

Note — one nomenclature, every surface. These three words are the names across the whole product, not just this API: the household's stored calendar preference and the app's /api/prefs, the agent's liturgy tool, and user-facing copy all use exactly ordinary / traditional / dominican. The earlier ids and aliases (roman-ordinary, roman-1962, general, of, tlm, op) were retired in one breaking rename before the API had external consumers — they 404 here, the prefs endpoint rejects them, and migration 0029 rewrote stored rows. Human synonyms ("TLM", "Latin Mass", "Extraordinary Form", "Novus Ordo") are natural language: the LLM layer maps them to the canonical word; parsers accept only the three (case-insensitively).


2. Resolution inputs: rite, year, region

Temporale — moveable, anchored on Easter (→ Septuagesima, Ash Wednesday, Ascension, Pentecost, the Sunday counts; Advent anchors the other end). A function of the year.

Note — Easter is computed. By the Gregorian (Western) computus; every moveable date depends on it, so it is pinned to a named algorithm + a regression table of known dates. Eastern (Julian) computus and divergent historical reckonings are out of scope.

Sanctorale — three kinds, not just "month+day":

{ "id": "sacred-heart", "kind": "moveable",
  "anchor": { "base": "corpus-christi-octave", "offset": "friday-after" } }
// base: a named temporale point (easter | pentecost | corpus-christi | advent | epiphany |
//   christmas | <-octave variants>);  offset: nth-weekday-after/before | sunday-within | n-days

Note — editions diff-layered internally, only merged results ship. A version is a base table + reform layers (1568 → … → 1960) — a build technique, not an exposed axis. No year-of-rubrics selector (can't ask for "1939"); edition-selection is future (Boundaries).

Region — three rubrical effects, two modelled: which holydays transfer to a Sunday (per-conference: Epiphany/Ascension/Corpus Christi), which are of obligation (§3a). It also confers proper feasts (patrons) — not modelled (Boundaries).

Note — region is narrower than a conference's real role. Proper feasts it confers do affect precedence (a patron-Solemnity changes the day) but we model only transfer + precept. The general default is the editio typica, not "most common" — it keeps Ascension/ Epiphany/Corpus Christi on the weekday though most of the world observes the Sunday; reading transfer/obligation off the default gives the typical-edition answer, not a country's. Per-conference tables are data, added over time.

Liturgical year — starts at the 1st Sunday of Advent (a civil year spans two); the OF cycles key off it:

"liturgical_year": {
  "starts": "2025-11-30",
  "sunday_cycle":  "C",   // OF A|B|C — rolls over at ADVENT;  null for 1962
  "weekday_cycle": "II",  // OF I|II  — rolls over at 1 JAN;   null for 1962
  "psalter_week":  2      // OF 1–4   — null when overridden
}

Note — the two cycles roll over on different dates. Sunday cycle turns at Advent; weekday cycle (I = odd civil years) at 1 January, mid-year — one starts can't derive both, so both are carried resolved. Cycles null for 1962 (annual). psalter_week is null far beyond 1962: Solemnities/Feasts take Week I, and Octaves + the final days of Advent/Holy Week override the psalter — a hint for ordinary days only, never a flat 1–4.


3. The liturgical day

The day endpoint returns the summary (the cheap core + links); Office, full Mass propers, and resolution are separate requests (the table above). The summary:

{
  "calendar": "traditional", "region": "general",
  "region_default_applied": true,  // true only when the caller named no region (§2)
  "editions": { "la": "vulgate", "en": "douay" },  // scripture edition per language, always present (§10)
  "date": "2026-01-28",       // CIVIL date (see note)
  "weekday": "wednesday",     // CIVIL weekday of the civil date
  "liturgical_year": { /* §2 */ },
  "season":     { "id": "epiphany", "arc": "after-epiphany", "week": 3 },  // §5
  "obligation": { /* §3a */ },
  "celebration":    { /* below — the headline, singular */ },
  "saints": {                  // §6 — THE field for "whose feast day is it?"
    "celebrated":   [ { "name": {…}, "id": "…", "person": "…", "rank": {…} } ],
    "commemorated": [ /* sanctoral commemorations; never the feria */ ],
    "remembered":   [ { "name": "Ss. Primus and Felician" }, … ]  // notes via ?include=martyrology
  },
  "masses": [ { "id": "day", "name": {…}, "colour": {…}, "option_type": "prescribed" } ],
  "links": { "office_url": "…", "masses_url": "…", "resolution_url": "…" }
}

Note — saints is keyed by what happened. celebrated/commemorated are THIS calendar's answer to "whose feast is it?" (a feria is not a saint and never appears); remembered is the Roman Martyrology — EVERY saint of the date (often 5–15), celebrated anywhere or not (vendored public-domain 1916 edition — saints canonised since are absent from it, while the calendar layer carries them; its dates follow the old calendar where they differ). Future resolution outcomes (transferred, omitted) arrive as new keys, additively. remembered[].name is English-only (Boundaries). Names are always present; each entry's eulogy note folds in with ?include=martyrology (heavy include, span-capped like masses/office — eulogies across a 90-day range would be most of the response's bytes). Responses are gzip-compressed when negotiated.

Note — the summary lists Mass descriptors, not a count. masses[] carries the cheap per-Mass { id, name, colour, option_type } (no propers — those come via masses_url or ?include=masses). A bare count would force a second call just to learn the Mass identities, and worse: celebration.colour is the day's colour, which disagrees with a multi-Mass day where each Mass has its own (a Requiem is black while the day reads white, §7). The per-Mass colour here is authoritative for rendering a day cell.

Note — civil date is the key; two distinct "Saturday-evening" facts. (Open.) We key on the civil date (re-keying would make "give me 2026-06-09" ambiguous) and model the evening spillover as explicit relations, kept separate: first Vespers (rubrical — the next day's Office is said this evening; liturgical_evening.belongs_to) vs anticipated Mass (canonical — a Saturday-evening Mass fulfils the Sunday precept; anticipated_mass). The Office may be tomorrow's while the Mass said is still today's. Open: their exact shape.

3a. Obligation — region-dependent, not derivable from rank.

"obligation": {
  "status": "lapsed",                        // yes | no | lapsed
  "reason": "falls on a Monday in region X", // display
  "reason_code": "lapses-monday"   // null | lapses-saturday | lapses-monday | transferred | not-precept-in-region
}

Note — tri-state + machine handle. A bool can't carry the partial-precept rule (lapses on a Saturday/Monday) without flipping year-to-year; reason_code is the programmatic handle beside the prose. At general (the typica) the answer is the universal law: every Sunday plus the canon-1246 ten (the fixed eight and the moveable Ascension/Corpus Christi), with reason naming which. NOT derivable from rank — most Solemnities carry no precept. Per-region lapse/transfer (reason_code, lapsed) fills in with the region tables.

Celebration

{
  "name":  { "la": "S. Petri Nolasci Confessoris", "en": "St. Peter Nolasco, Confessor" },
  "rank":  { "label": "class-3", "precedence": 711, "cycle": "sanctorale" },  // §4
  "colour": { "default": "white", "alternatives": [] },                       // §5
  "id":     "petrus-nolascus",     // RITE-SCOPED slug → /{calendar}/saints/{id}
  "person": "peter-nolasco",       // canonical link UP → /liturgy/v1/saints/{person} (§11) — mandatory
  "titles": ["confessor"],         // category SET → Common(s) (§8)
  "octave": null                   // or { "of", "day" } (§9)
}

Note — the slug is rite-scoped; person links up. The same saint can differ in rank, name, even date across rites, so id identifies this rite's celebrationperson is the mandatory link up to the canonical cross-rite record (§11), whose calendars{} links back down. The two slug namespaces never collide because each id is always paired with the other.


4. Rank, precedence, resolution

The most codified part. Two questions: occurrence (two offices, same date — who is celebrated/commemorated/omitted/transferred) and concurrence (one office's second Vespers meets the next's first Vespers — who owns it; §8).

Grades differ by rite. 1962/Dominican: four classes I–IV + commemorations + feria; feast-class and day-class (Sunday, feria, vigil, octave-day) interleave (a I-class feria outranks a I-class feast; a II-class Sunday outranks a III-class feast, then commemorated). OF: Solemnity > Feast > Memorial > Optional Memorial > Weekday (1969 table).

Note — "Sunday" is itself multi-rank. 1962: I-class Sundays (Advent/Lent/Passiontide, admit almost nothing) vs II-class (green Sundays a II-class feast can displace). OF parallels it (Advent/Lent/Easter Sundays outrank all feasts; Ordinary Sundays yield to Feasts of the Lord). A Sunday's rank carries its specific class, never one tier.

"rank": { "label": "class-1", "precedence": 910, "cycle": "temporale" }
// label = native grade; precedence = sortable (higher outranks); cycle = which PROPOSED it

Notes on rank.

Resolution (optional) — the decision, not just the winner:

"resolution": {
  "winner": "sanctorale",
  "displaced": [
    { "name": {…}, "outcome": "commemorated" },
    { "name": {…}, "outcome": "omitted" },
    { "name": {…}, "outcome": "not-observed" },  // dropped for the year
    { "name": {…}, "outcome": "transferred", "chain": ["2026-01-28","2026-01-30"] }
  ]
}

Transfer. An impeded feast of sufficient rank moves to the next free day — again if that is occupied (a chain: end-of-Advent, Christmas octave, Holy Week); if none before a cutoff, not observed this year. So a feast can arrive from an earlier date (transferred_from/transferred_to), shown on the new date's saints entries via from (§6). outcome distinguishes transferred (moved away) / not-observed / omitted (stayed, nothing said).

Note — resumed Sundays are a different motion. (Open.) An impeded Sunday after Epiphany/Pentecost can be resumed weeks later (before Advent) — transfer-like but not feast-shaped, so the chain doesn't fit. Unmodelled: it shows only on its celebrated date, no link back. Open: a resumes: relation.


5. Seasons and colour

"season": { "id": "lent", "arc": "after-epiphany", "week": 3 }
// arc: 1962 counting arc, null for OF;  week: within the arc (see note)

Ids: advent · christmas · (1962:) epiphany · septuagesima · passiontide · pentecost (arc after-pentecost) · (both:) lent · easter · (OF:) ordinary. Ascensiontide is within easter; the 1962 time after Pentecost is pentecost + arc, not a separate id. Weeks are 1-based and Sunday-anchored (the week of a mid-week arc start — Ash Wednesday's days, the days after Epiphany — is week 1). The OF Christmas season runs through the eve of the Baptism; 1962 Christmastide yields to Epiphanytide at 6 Jan. Seasons drive colour, the Mass chant swaps (§7), whether Gloria/Alleluia/Te Deum are said, the ferial/Common fallback.

Note — what week counts. Not a global counter. 1962 restarts per arc — after-epiphany and after-pentecost each start at 1 (so arc disambiguates); the after-Pentecost count varies 23–28 yearly, the last after-Epiphany Sundays resumed before Advent (§4). OF Ordinary Time is one 1–34 numbering that jumps across Lent–Easter (…8, [Lent/Easter], 11…); arc null.

Note — season is the celebrated position, scalar by choice. A day can sit in two frames (a Christmas-octave day is christmas + octave member; a resumed Sunday is in its pre-Advent slot while originating after Epiphany). One object (celebrated position), octave separate (§9); a resumed Sunday's origin frame is lost (open, §4). Rare enough.

Note — colour is { default, alternatives[] }, never scalar. Gaudete/Laetare add rose; a Requiem is black-or-violet; some vigils/votives differ. Values: white, red, green, violet, rose, black, gold.


6. Commemorations and the saints block

A lower, impeded office kept as a fragment of prayers in the winner's office — never "a second Mass".

"commemorations": [
  { "name": { "la": "S. Agnetis … secundo", "en": "St Agnes, 2nd feast" },
    "rank": { "label": "commemoration", "precedence": 110 },
    "kind": "ordinary", "prayers": { "collect": { … } }, "id": "agnes-secundo" }
]

Note — cap is computed; imperated collects are separate. The cap is per-day computed (season-dependent), not a constant 3. An imperated (commanded) collect (added by bishop/ rubrics) is not a saint-commemoration — own field imperated_collects[] on the Mass.

Note — Last Gospel in scope, Preface not. When a feast displaces a Sunday (1962), the commemorated Sunday's Gospel becomes the proper Last Gospel — calendar-selected, in scope (last_gospel). The day's Preface is rank/season-driven but belongs to the Ordinary — a Boundary (a label at most, not text).

saints — "whose feast is it?" A commemoration is the loser of a precedence contest, so the headline saint is never in saints.commemorated; and a loser is not always commemorated:

outcometheir feast on this date this year?where
celebratedyes — they wonsaints.celebrated (and celebration, the singular headline)
commemoratedyes — observed minimallysaints.commemorated
omittedyes — date is theirs, nothing said (admit-none days / cap exceeded)resolution now; saints.omitted when transfer chains land (additive)
arrivedyes — transferred in from an earlier datesaints.celebrated/commemorated, gaining a from date (additive)
transferred (away)no — moved off this dateorigin's resolution, not here

The block is keyed by outcome rather than tagging a flat list, so the common question ("who is celebrated today?") is one key access, and new outcomes extend the shape without breaking it. The Martyrology's remembered sits alongside as the third tier: not a resolution outcome at all, but the Church's full remembrance of the date.

"saints": {
  "celebrated":   [ { "name": {…}, "id": "lawrence", "person": "lawrence", "rank": {…} } ],
  "commemorated": [ { "name": {…}, "id": "agnes-secundo", "rank": {…} } ],
  "remembered":   [ { "name": "St Tychicus" } ]  // + "note" with ?include=martyrology
}

7. Mass

The propers (vs the fixed Ordinary), ordered: Introit · Collect · Epistle · Gradual/Tract/Alleluia · [Sequence] · Gospel · Offertory · Secret · Communion · Postcommunion. Several are scripture readings (OF: first/second + psalm; traditional: epistle/gospel); the chant texts (Introit, Gradual, Offertory, Communion) are proper antiphons. Each slot is tagged by role, so one shape serves every rite:

{ "role": "gospel", "ref": "Luc 14:16-24",
  "text": { "la": "In illo témpore…", "en": "…" },
  "edition": { "la": "vulgate", "en": "douay" } }  // which edition produced each text (ids per /liturgy/v1/translations)
// ref = uncopyrightable citation, present when known, independent of text + translation

Note — translation is a per-text axis, orthogonal to language (§10). Only scripture text carries it; the ref, names, rubrics, and chant antiphons do not. edition reports the translation actually used per language (after the translations request + per-language default fallback), so a client that asked for one and got another (unavailable for that passage) can tell. All editions we serve are public-domain and held internally.

Note — readings[] is variable-length + ordered, not a fixed Epistle/Gospel template: an Ember Saturday has up to six lessons, the Easter Vigil a series of prophecies (12 older / 4 in 1962); multiple role: "lectio" entries distinguished by position.

Note — interlection chant + Sequence. The between-readings slot is one role (interlection) varying by season — Gradual / Tract (penitential) / double Alleluia (Eastertide). The Sequence is a separate slot following the Alleluia: role: "sequence", obligation: obligatory | optional (Easter/Pentecost obligatory; Lauda Sion/ Stabat Mater optional; Dies Irae Requiem-bound).

One date, several Massesmasses is a list (singular = a list of one):

"masses": [
  { "id": "midnight", "name": {…}, "option_type": "prescribed", "colour": {…}, "readings": [ … ] },
  { "id": "dawn", "option_type": "prescribed", … }, { "id": "day", "option_type": "prescribed", … }
]
// option_type: prescribed (all said) | optional (a choice)

8. Office (Divine Office / Liturgy of the Hours)

Eight hours: matins · lauds · prime · terce · sext · none · vespers · compline (Prime suppressed in OF; a calendar advertises its hours). Each: psalms/ canticles framed by antiphons; hymn; chapter + versicle + responsory; collect. Proper gospel canticles by hour — Benedictus (Lauds), Magnificat (Vespers), Nunc Dimittis (Compline). Matins is larger — one/three nocturns of psalms + lessons, Te Deum on festive days.

{
  "hour": "vespers",
  "psalmody": [ { "antiphon": {…}, "psalm": { "ref": "Ps 109", "text": {…} } } ],
  "chapter": { "ref": "…", "text": {…} }, "hymn": {…}, "versicle": {…},
  "canticle": { "antiphon": {…}, "text": { "la": "Magnificat…", "en": "…" } },
  "collect": {…}, "commemorations": [ /* §6 — Lauds & Vespers only */ ]
}

Assembly by layersProper → applicable Common(s) → Psalter, each element falling through independently (collect from the Proper, psalms from the Psalter, etc.).

Note — the Common layer is composite. titles is a set (["martyr","bishop"] → both Commons compose) and each Common holds several formularies by season — "resolve across the applicable Commons, pick the seasonally-correct formulary", not "one title → one Common".

First vs second Vespers (concurrence) — a feast has a first Vespers the eve before and a second Vespers the day itself; where they meet, precedence (§4) decides which is sung and whether the other is commemorated. "Vespers on D" is ambiguous — the two are distinct objects.

Note — split-source Vespers is a Boundary. Concurrence isn't always a clean swap: psalms from one office, chapter/hymn/collect a capitulo from another — one Vespers from two offices, which Proper→Common→Psalter can't express. We serve the resolved Vespers + outcome, not the element split.


9. Octaves, vigils, privileged-season days

Octaves — a first-rank feast prolonged eight days; each infra octavam day and the dies octava is its own ranked, impedable day. 1960 keeps three (Christmas, Easter, Pentecost). octave: { of, day }day is a position; the actual class is in rank (Christmas-octave days are not uniform: 26–28 Dec III-class, Octave Day II-class).

Note — octave type out of scope at 1960+. Pre-1955 classified octaves (privileged/ common/simple); 1960 flattened to three, so no type today. A pre-1955 edition (§2) would need octave_type.

Vigils — the liturgical eve of a great feast, a separate day on the prior date with its own Mass/office; can fall within another octave.

Note — vigils are typed. vigil_type: ordinary (commemorated when impeded) | privileged (Christmas Eve, outranks a Sunday) | paschal (Easter Vigil — start of the Triduum). Rank is a property of the day (rank).

Privileged-season temporale days — day-types with their own proper Masses, ranked and commemoration-privileged (rank.cycle: "temporale"), not plain ferias: Advent ferias from 17 Dec (the "O" days); Lenten weekday propers; Ember Days (Quattuor Tempora); Rogation Days (Major Litany 25 Apr + Minor Litanies) — procession: true.

Note — the processional litany is unserved content. The Litany of the Saints in procession is neither Mass proper nor Office hour — a fourth content type with no home in §7/§8. Day/Mass/rank are in scope; procession: true flags it; the litany text is out.


10. Multilingual text

Every human-readable string is a language map { "la": …, "en": …, "fr": … }:

Language vs translation — two axes. Language (Accept-Language) picks the key in the map; translation (translations query param) picks, for scripture text only, which public-domain edition fills that key. They compose: Accept-Language: en + translations=cpdv → the English text rendered from the CPDV. Editions are data too (per language: en → douay | cpdv; la → vulgate), each with a per-language default; the resolved choice is reported on every reading via edition (§7) and echoed at the response level as editions. Non-scripture strings (names, rubrics, chant antiphons) have a single canonical rendering per language and ignore the axis. All editions are held internally (public-domain corpora vendored in the service); none is fetched from a third party.


11. Saints

Two levels. A rite-scoped entry (§3) is that rite's celebration — its own date, rank, name. Above it sits a canonical person that indexes the same saint across rites, so you can see all calendars at once — including when the rites keep different dates (Catherine of Siena: OF 29 Apr, 1962 30 Apr; Ephrem: OF 9 Jun, 1962 18 Jun). The person is an index, not a flattened truth: each rite's data stays its own. Unification is name normalisation (honorifics/title clauses stripped, orthographic variants folded) plus a name-variant merge guarded by feast-date proximity — two different saints sharing a name (the many Felixes) can never false-merge — plus a small editorial alias list for reform-shifted names the guard rightly refuses (Bridget of Sweden's Oct→Jul move).

Person — all calendars at once (GET /liturgy/v1/saints/john-of-the-cross):

{
  "person": "john-of-the-cross", "name": { "la": "S. Ioannes a Cruce", "en": "St John of the Cross" },
  "calendars": {
    "traditional":     { "id": "ioannes-a-cruce", "date": "11-24", "rank": "class-3",
                        "titles": ["confessor","doctor"], "ref": "/liturgy/v1/calendars/traditional/saints/ioannes-a-cruce" },
    "ordinary": { "id": "john-of-the-cross", "date": "12-14", "rank": "memorial",
                        "titles": ["doctor"], "ref": "/liturgy/v1/calendars/ordinary/saints/john-of-the-cross" }
  }
}

Each rite block carries both its local id and a ref (the down-link); the rite-scoped entry carries person (the up-link, §3). The two slug namespaces never strand a client.

Search disambiguates the several St Johns — name alone collides, so each carries titles

// GET /liturgy/v1/saints?q=john   (Accept-Language: en)
{ "results": [
    { "person": "john-the-baptist", "name": {…}, "titles": ["precursor"], "calendars": ["traditional","ordinary"],
      "feasts": [ { "date": "06-24", "kind": "nativity" }, { "date": "08-29", "kind": "beheading" } ] },
    { "person": "john-of-the-cross", "name": {…}, "titles": ["doctor"], "calendars": ["traditional","ordinary"] }
  ],
  "query": { "q": "john" }, "page": { "total": 2, "next": null } }

Boundaries (not served)

Open questions

Sources

Ranking of liturgical days · Commemoration · Occurrence · Proper · Roman Breviary · General Roman Calendar 1960 · 1960 General Rubrics · romcal (calendar-model prior art).

Data provenance (all held internally — no runtime third-party calls). Calendar + propers + Office: the public-domain Divinum Officium corpus, vendored. Martyrology: the public-domain 1916 Baltimore edition, vendored (data/martyrology/). Scripture text: public-domain editions, vendored — la: Clementine Vulgate (· Nova Vulgata where free); en: Douay-Rheims (Challoner), Catholic Public Domain Version. Citations are uncopyrightable facts. Nothing is fetched from an external service at request time.