@escape-game-over/atlas 0.1.112 → 0.1.113

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -58,40 +58,58 @@ Linking to a page the active deployment switched off is a compile error, not a
58
58
 
59
59
  **2. A deployment is an overlay, not a fork.**
60
60
 
61
- One file per deployment, saying only what differs:
61
+ A few files per deployment, saying only what differs. Its copy first, the
62
+ languages it publishes and what it rewords or adds:
62
63
 
63
64
  ```ts
64
- export const { project, redirects } = atlas.project(
65
- defaultMessages,
66
- defaultRoutes,
67
- {
68
- url: "https://acme.example",
69
- siteName: "Acme Rome",
70
- icon,
71
- themeColor: "#1d51e0",
72
-
73
- enabledLocales: ["el-GR", "en-US"],
74
- overrideRouting: { defaultLocale: "el-GR", prefixDefaultLocale: true },
75
- formats: {
76
- clock: { "el-GR": "h23", "en-US": "h12" }, // how each language prints times
77
- prices: { decimals: "always" }, // "18,00 €", each page in its own locale
65
+ // projects/rome/messages.ts
66
+ export const messages = atlas.catalog(defaultMessages, {
67
+ locales: ["el-GR", "en-US"],
68
+ override: {
69
+ // Greek only — the English copy still falls through to the default catalog.
70
+ "home.hero.body": { "el-GR": "Η {company} κρατά το απόθεμα…" },
71
+ },
72
+ add: {
73
+ // Copy only this deployment has, in every language it publishes.
74
+ "venue.parking": {
75
+ "el-GR": "Πάρκινγκ στη γωνία",
76
+ "en-US": "Parking round the corner",
78
77
  },
78
+ },
79
+ });
80
+ ```
79
81
 
80
- overrideMessages: {
81
- // Greek only — the English copy still falls through to the default catalog.
82
- "home.hero.body": { "el-GR": "Η {company} κρατά το απόθεμα…" },
83
- },
84
- overrideRoutes: {
85
- careers: { enabled: true }, // opt into a page
86
- contact: { slugByLocale: { "el-GR": "epikoinoniste-mazi-mas" } }, // retranslate a URL
87
- },
88
- }
89
- );
82
+ Then the project, beside it:
83
+
84
+ ```ts
85
+ // projects/rome/project.ts
86
+ export const { project, redirects } = atlas.project(messages, defaultRoutes, {
87
+ url: "https://acme.example",
88
+ siteName: "Acme Rome",
89
+ icon,
90
+ themeColor: "#1d51e0",
91
+
92
+ overrideRouting: { defaultLocale: "el-GR", prefixDefaultLocale: true },
93
+ formats: {
94
+ clock: { "el-GR": "h23", "en-US": "h12" }, // how each language prints times
95
+ prices: { decimals: "always" }, // "18,00 €", each page in its own locale
96
+ },
97
+
98
+ overrideRoutes: {
99
+ careers: { enabled: true }, // opt into a page
100
+ contact: { slugByLocale: { "el-GR": "epikoinoniste-mazi-mas" } }, // retranslate a URL
101
+ },
102
+ });
90
103
  ```
91
104
 
92
105
  Every field is named for what it _does_ to the defaults, so a project file can
93
- never be mistaken for the whole truth: `enabledLocales` picks from what the site
94
- declares, and every `override*` is a patch over it.
106
+ never be mistaken for the whole truth: `locales` picks from what the site
107
+ declares, and `override` and every `override*` is a patch over it.
108
+
109
+ The copy comes first so a deployment's other files can name its messages by
110
+ key. Anything declared before the project, its venues or its settings, types a
111
+ message as `CatalogKey<typeof messages>`: the default catalog's keys and the
112
+ deployment's own, exactly the ones `t()` takes. A typo fails on its line.
95
113
 
96
114
  ## The four steps
97
115
 
@@ -106,11 +124,12 @@ export const atlas = defineSite({ locales: {...}, defaultRouting: {...} })
106
124
  export const baseMessages = atlas.messages({...})
107
125
  export const baseRoutes = atlas.routes({...})
108
126
 
109
- // 3. one deployment, and its redirects, typed against what it builds
110
- export const { project, redirects } = atlas.project(baseMessages, baseRoutes, {...})
127
+ // 3. one deployment's copy, then the deployment and its redirects
128
+ export const messages = atlas.catalog(baseMessages, { locales: [...], override: {...} })
129
+ export const { project, redirects } = atlas.project(messages, baseRoutes, {...})
111
130
 
112
- // 4. the API the pages use
113
- export const site = atlas.site(baseMessages, baseRoutes, project)
131
+ // 4. the API the pages use: the deployment's catalog, the routes, the deployment
132
+ export const site = atlas.site(messages, baseRoutes, project)
114
133
  ```
115
134
 
116
135
  A site that publishes one language under several regions writes the words once:
@@ -246,7 +265,7 @@ regression-tested by an `@ts-expect-error` suite.
246
265
  | A page with no share image, alt text, description or `robots` directives | error — none of them have a default |
247
266
  | A `robots` directive that is not a real one | error — the vocabulary is a union |
248
267
  | A project `url` that is not `https://`, or ends in a slash | error |
249
- | `defaultLocale` not among the project's `enabledLocales` | error |
268
+ | `defaultLocale` not among the locales the project's catalog publishes | error |
250
269
  | A `formats.clock` leaving out a published locale, or naming an unpublished one | error |
251
270
  | `site.clockFor` on a project that declares no clock | throws, naming the project |
252
271
  | `site.priceFormatFor` on a project that declares no price format | throws, naming the project |
package/docs/checks.md CHANGED
@@ -37,20 +37,20 @@ conditional's result, but goes structural — `Property '__X__' is missing` —
37
37
  it is an annotation, and there the property name is the only part the reader
38
38
  sees.
39
39
 
40
- | Compile error | Build-time throw |
41
- | ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
42
- | unknown message key, or an override dropping a `{placeholder}` | two routes resolving to the same URL |
43
- | a message the project declared mandatory and never wrote (`CatalogCovers`) | |
44
- | copy or a locale label written for a locale outside `enabledLocales` | |
45
- | a `formats.clock` leaving out a published locale, or naming one outside `enabledLocales` | the same, for a project that was not type-checked first |
46
- | a `formats.prices.locale` the project does not publish | the same, for a project that was not type-checked first |
47
- | a slug whose translation sits at a different depth, in the registry or across the overlay | the same, when a slug is not a literal (computed, or from an env var) |
48
- | unknown route id, or a locale the site does not ship | a locale published twice |
49
- | a malformed language tag (`en_US`, `en-us`, `EN-US`, bare `en`) | an icon that is not square, a multiple of 48, and PNG |
50
- | `defaultLocale` outside `locales`; unknown top-level field | a `url` that is not `https://` or ends in `/`, when it is not a literal |
51
- | a `url` literal that is not `https://` or ends in `/` | |
52
- | a `lastmod` that is not a `YYYY-MM-DD` calendar date | |
53
- | config for a route this project does not build, or missing for one it does (`PerRoute`) | |
40
+ | Compile error | Build-time throw |
41
+ | -------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
42
+ | unknown message key, or an override dropping a `{placeholder}` | two routes resolving to the same URL |
43
+ | a message the project declared mandatory and never wrote (`CatalogCovers`) | |
44
+ | copy or a locale label written for a locale its catalog does not publish | |
45
+ | a `formats.clock` leaving out a published locale, or naming one its catalog does not publish | the same, for a project that was not type-checked first |
46
+ | a `formats.prices.locale` the project does not publish | the same, for a project that was not type-checked first |
47
+ | a slug whose translation sits at a different depth, in the registry or across the overlay | the same, when a slug is not a literal (computed, or from an env var) |
48
+ | unknown route id, or a locale the site does not ship | a locale published twice |
49
+ | a malformed language tag (`en_US`, `en-us`, `EN-US`, bare `en`) | an icon that is not square, a multiple of 48, and PNG |
50
+ | `defaultLocale` outside `locales`; unknown top-level field | a `url` that is not `https://` or ends in `/`, when it is not a literal |
51
+ | a `url` literal that is not `https://` or ends in `/` | |
52
+ | a `lastmod` that is not a `YYYY-MM-DD` calendar date | |
53
+ | config for a route this project does not build, or missing for one it does (`PerRoute`) | |
54
54
 
55
55
  The runtime checks are the ones types cannot see: facts about a _file_ (an icon's
56
56
  real dimensions), facts only known after merging (URL collisions), or a value
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@escape-game-over/atlas",
3
- "version": "0.1.112",
3
+ "version": "0.1.113",
4
4
  "type": "module",
5
5
  "description": "Typed, data-driven machinery for static multi-locale, multi-deployment Astro sites.",
6
6
  "private": false,
package/src/config.ts CHANGED
@@ -195,7 +195,7 @@ export interface LocaleNotDeclared<L extends string> {
195
195
 
196
196
  /** Editor-facing error for anything written for a locale a project does not ship. */
197
197
  export interface LocaleNotEnabled<L extends string> {
198
- readonly __LOCALE_NOT_ENABLED__: `locale "${L}" is not among this project's enabledLocales, so anything written for it would never be built`;
198
+ readonly __LOCALE_NOT_ENABLED__: `locale "${L}" is not among the locales this project's catalog publishes, so anything written for it would never be built`;
199
199
  }
200
200
 
201
201
  /**
@@ -0,0 +1,149 @@
1
+ import type {
2
+ LocaleNotEnabled,
3
+ LocalesOf,
4
+ SiteConfigShape,
5
+ } from "../config.ts";
6
+ import type { NoExcessKeys, StringKeys } from "../types.ts";
7
+ import type {
8
+ BaseCatalog,
9
+ OverrideCatalog,
10
+ ValidateAdditions,
11
+ ValidateOverrideCatalog,
12
+ } from "./define.ts";
13
+
14
+ /**
15
+ * One project's copy: the site's catalog, the locales the project publishes,
16
+ * and what it rewords or adds.
17
+ *
18
+ * Built before the project, and from nothing but copy, so the project's other
19
+ * files (its settings, its venues' data) can name a message by its exact key
20
+ * with `CatalogKey<typeof catalog>`. Were the copy declared inside the project,
21
+ * as it once was, a file the project is built from could only name keys as a
22
+ * string pattern: reaching the project's keys from there is a cycle.
23
+ */
24
+ export interface ProjectCatalog<Base, Published extends string, Override, Add> {
25
+ readonly base: Base;
26
+ /** The locales the project publishes, in the order it lists them. */
27
+ readonly locales: readonly Published[];
28
+ /** Rewordings of the base catalog's messages. */
29
+ readonly override: Override;
30
+ /** Messages only this project has. */
31
+ readonly add: Add;
32
+ }
33
+
34
+ /**
35
+ * Every message key a project's copy has: the base catalog's and its own.
36
+ *
37
+ * What a site built from the project translates, so a key typed with it is
38
+ * one `t()` takes as it is.
39
+ */
40
+ export type CatalogKey<C> =
41
+ C extends ProjectCatalog<infer Base, string, unknown, infer Add>
42
+ ? StringKeys<Base> | StringKeys<Add>
43
+ : never;
44
+
45
+ /** The messages a site built from the catalog reads: the base's and its own. */
46
+ export type MessagesOf<C> =
47
+ C extends ProjectCatalog<infer Base, string, unknown, infer Add>
48
+ ? Base & Add
49
+ : never;
50
+
51
+ /**
52
+ * What `override` may hold, as the editor offers it: the base catalog's keys,
53
+ * each written for the locales the project publishes. A key or a locale beyond
54
+ * these is named by `OverrideChecks`.
55
+ */
56
+ export type OverrideEntries<Base, Published extends string> = {
57
+ readonly [K in StringKeys<Base>]?: Readonly<
58
+ Partial<Record<Published, string>>
59
+ >;
60
+ };
61
+
62
+ /** What `add` may hold, as the editor offers it: new keys, published locales. */
63
+ export type AddedEntries<Published extends string> = Readonly<
64
+ Record<string, Readonly<Partial<Record<Published, string>>>>
65
+ >;
66
+
67
+ /** The locales a catalog publishes. */
68
+ export type PublishedOf<C> =
69
+ C extends ProjectCatalog<unknown, infer Published, unknown, unknown>
70
+ ? Published
71
+ : never;
72
+
73
+ /**
74
+ * What `override` must be: keys the base catalog declares, each written only
75
+ * for locales the project publishes. Copy for a locale it does not publish is
76
+ * never built, so it is always a mistake: a locale removed from the list, or a
77
+ * project copied from another.
78
+ */
79
+ export type OverrideChecks<Base, Published extends string, O> = NoExcessKeys<
80
+ O,
81
+ StringKeys<Base>
82
+ > & {
83
+ readonly [K in StringKeys<O>]: {
84
+ readonly [Locale in StringKeys<O[K]>]: Locale extends Published
85
+ ? O[K][Locale]
86
+ : LocaleNotEnabled<Locale>;
87
+ };
88
+ };
89
+
90
+ /**
91
+ * Declares one project's copy, validated against the base catalog.
92
+ *
93
+ * Enforced at compile time, each on the line that is wrong:
94
+ * - `locales` are ones the site ships;
95
+ * - `override` keys exist in the base catalog, keep its `{placeholders}` and
96
+ * `[marks]`, and are written only for published locales;
97
+ * - `add` keys are new to the base catalog, written for exactly the published
98
+ * locales, with the same `{placeholders}` and `[marks]` in each.
99
+ */
100
+ export function defineCatalog<
101
+ const C extends SiteConfigShape,
102
+ const Base extends BaseCatalog<LocalesOf<C>>,
103
+ const Published extends LocalesOf<C>,
104
+ const Override extends OverrideEntries<Base, Published> &
105
+ OverrideChecks<Base, Published, Override>,
106
+ const Add extends AddedEntries<Published> = Record<never, never>,
107
+ >(
108
+ // Read for its type only: it is how the site's locales are inferred.
109
+ _config: C,
110
+ base: Base,
111
+ // The whole-object validators sit here rather than in the constraints, so
112
+ // a mistake is reported on its own line instead of at this call.
113
+ own: {
114
+ readonly locales: readonly Published[];
115
+ readonly override: Override &
116
+ ValidateOverrideCatalog<Override, Base, LocalesOf<C>>;
117
+ readonly add?: Add & ValidateAdditions<Add, Base, Published>;
118
+ }
119
+ ): ProjectCatalog<Base, Published, Override, Add> {
120
+ // Checked at runtime as well as in the type: an Astro config is evaluated
121
+ // before the type check runs, so the error has to be readable from here.
122
+ if (own.override === undefined) {
123
+ throw new Error(
124
+ "A catalog declares no override. It is required even when nothing is reworded — write `override: {}`, which tells a reader the shared copy is used as it is."
125
+ );
126
+ }
127
+ const add: OverrideCatalog<Published> = own.add ?? {};
128
+ for (const [key, entry] of Object.entries(add)) {
129
+ if (key in base) {
130
+ throw new Error(
131
+ `add: "${key}" is already in the base catalog. Reword it in override instead.`
132
+ );
133
+ }
134
+ const missing = own.locales.filter(
135
+ (locale) => typeof entry[locale] !== "string"
136
+ );
137
+ if (missing.length > 0) {
138
+ throw new Error(
139
+ `add: "${key}" has no ${missing.join(", ")}. A message a project adds is written for every locale it publishes.`
140
+ );
141
+ }
142
+ }
143
+ return {
144
+ base,
145
+ locales: own.locales,
146
+ override: own.override,
147
+ add: add as Add,
148
+ };
149
+ }
@@ -103,7 +103,7 @@ export function defineMessages<
103
103
  return catalog;
104
104
  }
105
105
 
106
- /** What `defineProject` checks an `overrideMessages` against: placeholders and locales. */
106
+ /** What `defineCatalog` checks an `override` against: placeholders and locales. */
107
107
  export type ValidateOverrideCatalog<
108
108
  T,
109
109
  Base,
@@ -139,12 +139,12 @@ export type MessageOverrides<Catalog, L extends string> = {
139
139
  };
140
140
 
141
141
  /**
142
- * What `defineProject` checks an `addMessages` against: each key new to the
142
+ * What `defineCatalog` checks an `add` against: each key new to the
143
143
  * base catalog, written for exactly the locales the project publishes, with
144
144
  * the same `{placeholders}` and `[marks]` in each.
145
145
  *
146
146
  * New only, because a key the base already has is copy every project shares,
147
- * and rewording it is what `overrideMessages` is for — with its own check that
147
+ * and rewording it is what `override` is for — with its own check that
148
148
  * the placeholders survive. An addition shadowing a base key would get round
149
149
  * that check, and two fields could then say different things about one key.
150
150
  */
@@ -182,7 +182,7 @@ export function mergeCatalog<L extends string>(
182
182
  // Astro config is evaluated before the type check, so say it plainly.
183
183
  if (key in merged) {
184
184
  throw new Error(
185
- `addMessages: "${key}" is already in the default catalog. Reword it in overrideMessages instead.`
185
+ `add: "${key}" is already in the default catalog. Reword it in override instead.`
186
186
  );
187
187
  }
188
188
  merged[key] = { ...entry } as Record<string, string>;
@@ -114,7 +114,7 @@ export interface PlaceholderMismatch<Key extends string, L extends string> {
114
114
 
115
115
  /** Editor-facing error when a project adds a message the base already has. */
116
116
  export interface AddedMessageExists<Key extends string> {
117
- readonly __ADDED_MESSAGE_EXISTS__: `Message "${Key}" is already in the default catalog: reword it in overrideMessages instead of adding it`;
117
+ readonly __ADDED_MESSAGE_EXISTS__: `Message "${Key}" is already in the default catalog: reword it in override instead of adding it`;
118
118
  }
119
119
 
120
120
  /** Editor-facing error when an override drops or invents one. */
package/src/index.ts CHANGED
@@ -7,8 +7,9 @@
7
7
  * export const atlas = defineSite({ locales: {...}, defaultRouting: {...} }) // site.config.ts
8
8
  * export const defaultMessages = atlas.messages({...}) // messages.ts
9
9
  * export const defaultRoutes = atlas.routes({...}) // routes.ts
10
- * export const { project, redirects } = atlas.project(defaultMessages, defaultRoutes, {...}) // projects/<name>/project.ts
11
- * export const site = atlas.site(defaultMessages, defaultRoutes, project) // site.ts
10
+ * export const messages = atlas.catalog(defaultMessages, {...}) // projects/<name>/messages.ts
11
+ * export const { project, redirects } = atlas.project(messages, defaultRoutes, {...}) // projects/<name>/project.ts
12
+ * export const site = atlas.site(messages, defaultRoutes, project) // site.ts
12
13
  * ```
13
14
  */
14
15
 
@@ -176,6 +177,12 @@ export {
176
177
  type Weekday,
177
178
  type WeeklyHours,
178
179
  } from "./hours.ts";
180
+ export type {
181
+ CatalogKey,
182
+ MessagesOf,
183
+ ProjectCatalog,
184
+ PublishedOf,
185
+ } from "./i18n/catalog.ts";
179
186
  export type {
180
187
  BaseCatalog,
181
188
  CatalogCovers,
@@ -277,7 +284,6 @@ export {
277
284
  type PriceFormat,
278
285
  } from "./price-format.ts";
279
286
  export type {
280
- AddedCatalog,
281
287
  DefinedProject,
282
288
  ProjectInput,
283
289
  ProjectRouteId,
package/src/project.ts CHANGED
@@ -8,12 +8,8 @@ import type {
8
8
  } from "./config.ts";
9
9
  import type { Fonts } from "./fonts.ts";
10
10
  import type { HourCycle } from "./hours.ts";
11
- import type {
12
- BaseCatalog,
13
- OverrideCatalog,
14
- ValidateAdditions,
15
- ValidateOverrideCatalog,
16
- } from "./i18n/define.ts";
11
+ import type { ProjectCatalog, PublishedOf } from "./i18n/catalog.ts";
12
+ import type { BaseCatalog, OverrideCatalog } from "./i18n/define.ts";
17
13
  import type { ImageAsset } from "./image.ts";
18
14
  import type { SiteVerification, ThemeColor } from "./meta/index.ts";
19
15
  import type { PriceFormat } from "./price-format.ts";
@@ -46,7 +42,7 @@ export interface ProjectFormats<L extends string> {
46
42
  * The clock each published language prints times on: `"h23"` for 18:00,
47
43
  * `"h12"` for 6:00 PM. Read back with `site.clockFor(locale)`.
48
44
  *
49
- * One entry for every locale in `enabledLocales`, and none for any other,
45
+ * One entry for every locale its catalog publishes, and none for any other,
50
46
  * so what each page prints is written down beside the languages that print
51
47
  * it: a bilingual Canadian site reads `{ "en-CA": "h12", "fr-CA": "h23" }`.
52
48
  * Not taken from the locale's own default (`Intl`'s) on purpose: a site may
@@ -121,14 +117,6 @@ export interface ProjectInput<L extends string, Routes> {
121
117
  readonly themeColor: ThemeColor;
122
118
  /** Which colour schemes the pages support, e.g. `"light"`, `"dark light"`. */
123
119
  readonly colorScheme?: string;
124
- /**
125
- * Which of the site's locales this project publishes.
126
- *
127
- * A selection, not an override: the site config declares which languages
128
- * exist at all, and a project picks from them. Naming one it does not
129
- * declare is a compile error.
130
- */
131
- readonly enabledLocales: readonly L[];
132
120
  /**
133
121
  * How this site writes what it formats: times and prices. Each format is
134
122
  * optional, and its reader refuses to answer for a project that declares
@@ -150,41 +138,13 @@ export interface ProjectInput<L extends string, Routes> {
150
138
  /** Adds to the site's `defaultFonts`, or replaces one by key. */
151
139
  readonly overrideFonts?: Fonts;
152
140
  /**
153
- * Copy overrides: any subset of keys, any subset of locales.
141
+ * Slug retranslations and page on/off switches.
154
142
  *
155
143
  * **Required, and `{}` is the answer for a deployment that overrides
156
- * nothing.** Unlike the two fields above, which are optional.
157
- *
158
- * The reason is not that an absent overlay and an empty one read
159
- * differently — they do, and that argument would make all four of these
160
- * required. It is that these two are what the type machinery *computes
161
- * from*: the route ids a project builds, the message keys it may override,
162
- * and the locales each may be written for are all read off these fields,
163
- * at seven sites in this file. Optional means `undefined` flowing into
164
- * every one of them, `NonNullable<>` at each — see `overrideRouting` above,
165
- * the one optional field that is read, for what that looks like — and an
144
+ * nothing.** The route ids a project builds are computed from it, and an
145
+ * optional field would mean `undefined` flowing into each of them and an
166
146
  * inference that widens where it should narrow.
167
- *
168
- * `{}` costing two characters is the price of the ids downstream being
169
- * exact. It also happens to read as a statement, which is a bonus rather
170
- * than the argument.
171
147
  */
172
- readonly overrideMessages: OverrideCatalog<L>;
173
- /**
174
- * Copy only this project has: keys the default catalog does not declare,
175
- * each written for every locale the project publishes and no others.
176
- *
177
- * For what one deployment says and no other does — a venue's own FAQ, a
178
- * house rule — so it goes through `translate` and `rich` like any message,
179
- * marks and placeholders included, without the shared catalog carrying it
180
- * in languages the venue never publishes. A key the default catalog
181
- * already has is refused: reword it in `overrideMessages`.
182
- *
183
- * Optional, unlike `overrideMessages`: nothing downstream is computed from
184
- * it except the catalog's keys, which an absent field leaves as they are.
185
- */
186
- readonly addMessages?: OverrideCatalog<L>;
187
- /** Slug retranslations and page on/off switches. See `overrideMessages`. */
188
148
  readonly overrideRoutes: RouteOverrides<Routes, L>;
189
149
  }
190
150
 
@@ -217,60 +177,64 @@ export type ProjectRouteId<
217
177
  export interface DefinedProject<
218
178
  Routes extends EnabledFlagSource,
219
179
  T extends ProjectInput<string, Routes>,
180
+ Published extends string,
220
181
  > {
221
182
  readonly project: T;
222
183
  /** Rules for this project's old URLs, checked against what it builds. */
223
184
  readonly redirects: <
224
185
  const Rules extends readonly RedirectRule<
225
186
  ProjectRouteId<Routes, T>,
226
- T["enabledLocales"][number]
187
+ Published
227
188
  >[],
228
189
  >(
229
190
  rules: Rules & ValidateRedirectTargets<Rules>
230
- ) => ProjectRedirects<
231
- ProjectRouteId<Routes, T>,
232
- T["enabledLocales"][number]
233
- >;
191
+ ) => ProjectRedirects<ProjectRouteId<Routes, T>, Published>;
234
192
  }
235
193
 
236
194
  /**
237
195
  * A project's `redirects`. The one place a rule list is branded as checked.
238
196
  *
239
- * Takes the routes and the project only for their types; the rules come back
240
- * as they went in.
197
+ * Takes the routes, the project and its catalog only for their types; the
198
+ * rules come back as they went in.
241
199
  */
242
200
  export function bindRedirects<
243
201
  Routes extends EnabledFlagSource,
244
202
  T extends ProjectInput<string, Routes>,
245
- >(_routes: Routes, _project: T): DefinedProject<Routes, T>["redirects"] {
203
+ Published extends string,
204
+ >(
205
+ _routes: Routes,
206
+ _project: T,
207
+ _catalog: ProjectCatalog<unknown, Published, unknown, unknown>
208
+ ): DefinedProject<Routes, T, Published>["redirects"] {
246
209
  return (rules) => rules as never;
247
210
  }
248
211
 
249
212
  /**
250
- * Declares one deployment, validated against the site config and base data.
213
+ * Declares one deployment, validated against the site config, its catalog
214
+ * and the routes.
251
215
  *
252
216
  * Everything is inferred from the values passed in, so a project file writes no
253
217
  * type arguments at all.
254
218
  *
255
219
  * Enforced at compile time:
256
220
  * - `url` must not end with a slash;
257
- * - `overrideMessages` keys must exist in the default catalog, and must keep
258
- * the default message's `{placeholders}`;
259
221
  * - `overrideRoutes` keys must be real route ids;
260
222
  * - unknown top-level fields are rejected, so a typo cannot sit there unread;
261
- * - locales named anywhere must be ones the site ships and this project enabled.
223
+ * - locales named anywhere must be ones its catalog publishes.
224
+ *
225
+ * Its copy is checked by `atlas.catalog()`, before the project exists.
262
226
  */
263
227
  /** Everything a project file must satisfy; see `defineSite`. */
264
228
  export type ProjectChecks<
265
229
  L extends string,
266
- Catalog,
230
+ Published extends string,
267
231
  Routes,
268
232
  T extends ProjectInput<L, Routes>,
269
233
  > = NoExcessKeys<T, keyof ProjectInput<L, Routes>> & {
270
234
  readonly url: ValidSiteUrl<T["url"]>;
271
235
  readonly overrideRouting?: {
272
- readonly defaultLocale?: T["enabledLocales"][number];
273
- // Against `enabledLocales` rather than the site's list: a
236
+ readonly defaultLocale?: Published;
237
+ // Against the published locales rather than the site's list: a
274
238
  // segment spelled for a language this deployment does not
275
239
  // publish is a word that appears in no URL it builds. Mapped
276
240
  // over the keys written, for the reason `overrideLocaleMeta`
@@ -278,26 +242,7 @@ export type ProjectChecks<
278
242
  readonly pageSegmentByLocale?: {
279
243
  readonly [K in StringKeys<
280
244
  NonNullable<T["overrideRouting"]>["pageSegmentByLocale"]
281
- >]: K extends T["enabledLocales"][number]
282
- ? string
283
- : LocaleNotEnabled<K>;
284
- };
285
- };
286
- // Two separate checks on the same field: the keys must be messages
287
- // the default catalog declares, and the locales inside each must be
288
- // ones this project publishes. Copy written for a locale left out of
289
- // `enabledLocales` is never built, so it is always a mistake — a
290
- // locale removed from the list, or a project copied from another.
291
- readonly overrideMessages: NoExcessKeys<
292
- T["overrideMessages"],
293
- StringKeys<Catalog>
294
- > & {
295
- readonly [K in StringKeys<T["overrideMessages"]>]: {
296
- readonly [L in StringKeys<
297
- T["overrideMessages"][K]
298
- >]: L extends T["enabledLocales"][number]
299
- ? T["overrideMessages"][K][L]
300
- : LocaleNotEnabled<L>;
245
+ >]: K extends Published ? string : LocaleNotEnabled<K>;
301
246
  };
302
247
  };
303
248
  readonly overrideRoutes: NoExcessKeys<
@@ -310,9 +255,7 @@ export type ProjectChecks<
310
255
  // part of `T` and there is no fresh object for excess-property
311
256
  // checking to fire on.
312
257
  readonly overrideLocaleMeta?: {
313
- readonly [K in StringKeys<
314
- T["overrideLocaleMeta"]
315
- >]: K extends T["enabledLocales"][number]
258
+ readonly [K in StringKeys<T["overrideLocaleMeta"]>]: K extends Published
316
259
  ? NoExcessKeys<T["overrideLocaleMeta"][K], keyof LocaleMetaOverride>
317
260
  : LocaleNotEnabled<K>;
318
261
  };
@@ -324,13 +267,11 @@ export type ProjectChecks<
324
267
  readonly clock?: {
325
268
  readonly [K in StringKeys<
326
269
  NonNullable<T["formats"]>["clock"]
327
- >]: K extends T["enabledLocales"][number]
328
- ? HourCycle
329
- : LocaleNotEnabled<K>;
270
+ >]: K extends Published ? HourCycle : LocaleNotEnabled<K>;
330
271
  };
331
272
  // Its locale too must be one this project publishes.
332
273
  readonly prices?: PriceFormat & {
333
- readonly locale?: PriceLocaleCheck<T>;
274
+ readonly locale?: PriceLocaleCheck<T, Published>;
334
275
  };
335
276
  };
336
277
  };
@@ -343,122 +284,99 @@ type PriceLocaleOf<T> = T extends {
343
284
  : never;
344
285
 
345
286
  /** The price locale, if the project publishes it; else an error naming it. */
346
- type PriceLocaleCheck<T> = T extends {
347
- readonly enabledLocales: readonly (infer Published)[];
348
- }
349
- ? PriceLocaleOf<T> extends Published
287
+ type PriceLocaleCheck<T, Published extends string> =
288
+ PriceLocaleOf<T> extends Published
350
289
  ? PriceLocaleOf<T>
351
- : LocaleNotEnabled<PriceLocaleOf<T>>
352
- : never;
290
+ : LocaleNotEnabled<PriceLocaleOf<T>>;
353
291
 
354
292
  /** The same checks that map the whole override objects, in parameter position. */
355
293
  export type ProjectOverrideChecks<
356
- L extends string,
357
- Catalog,
294
+ Published extends string,
358
295
  Routes,
359
- T extends ProjectInput<L, Routes>,
296
+ T extends ProjectInput<string, Routes>,
360
297
  > = {
361
- readonly overrideMessages: ValidateOverrideCatalog<
362
- T["overrideMessages"],
363
- Catalog,
364
- L
365
- >;
366
298
  readonly overrideRoutes: ValidateOverrideSlugDepth<
367
299
  Routes,
368
300
  T["overrideRoutes"]
369
301
  >;
370
- } & AdditionsCheck<Catalog, T> &
371
- ClockCheck<T>;
372
-
373
- /** A project's own messages, checked like the catalog's: when it adds any. */
374
- type AdditionsCheck<
375
- Catalog,
376
- T extends ProjectInput<string, unknown>,
377
- > = T extends { readonly addMessages: infer A }
378
- ? {
379
- readonly addMessages: ValidateAdditions<
380
- A,
381
- Catalog,
382
- T["enabledLocales"][number]
383
- >;
384
- }
385
- : unknown;
302
+ } & ClockCheck<T, Published>;
386
303
 
387
304
  /**
388
305
  * A clock, once declared, is declared for every page: a locale left out would
389
306
  * have no clock to print its times on.
390
307
  */
391
- type ClockCheck<T extends ProjectInput<string, unknown>> = T extends {
308
+ type ClockCheck<T, Published extends string> = T extends {
392
309
  readonly formats: { readonly clock: object };
393
310
  }
394
311
  ? {
395
312
  readonly formats: {
396
313
  readonly clock: {
397
- readonly [K in T["enabledLocales"][number]]: HourCycle;
314
+ readonly [K in Published]: HourCycle;
398
315
  };
399
316
  };
400
317
  }
401
318
  : unknown;
402
319
 
403
- /** A project's own messages, as a catalog: none when it adds none. */
404
- export type AddedCatalog<P> = P extends { readonly addMessages: infer A }
405
- ? A
406
- : Record<never, never>;
320
+ /**
321
+ * The catalog each project was built with, so the site can check it is given
322
+ * the same one. Weak, so a project nothing holds any more is not kept alive.
323
+ */
324
+ const builtWith = new WeakMap<object, object>();
325
+
326
+ /**
327
+ * Throws if a site is given a catalog other than the one its project was built
328
+ * with: the project's locale checks were made against that one. A project
329
+ * assembled by hand, which `defineProject` never saw, has nothing to compare.
330
+ */
331
+ export function checkProjectCatalog(
332
+ project: { readonly siteName: string },
333
+ catalog: object
334
+ ): void {
335
+ const own = builtWith.get(project);
336
+ if (own !== undefined && own !== catalog) {
337
+ throw new Error(
338
+ `Project "${project.siteName}" was built with a different catalog than the one passed to atlas.site(): pass the catalog its project.ts uses.`
339
+ );
340
+ }
341
+ }
407
342
 
408
343
  export function defineProject<
409
344
  const C extends SiteConfigShape,
410
- const Catalog extends BaseCatalog<LocalesOf<C>>,
345
+ const Cat extends ProjectCatalog<
346
+ BaseCatalog<LocalesOf<C>>,
347
+ LocalesOf<C>,
348
+ OverrideCatalog<LocalesOf<C>>,
349
+ OverrideCatalog<LocalesOf<C>>
350
+ >,
411
351
  const Routes extends RouteRegistry<LocalesOf<C>>,
412
352
  const T extends ProjectInput<LocalesOf<C>, Routes> &
413
- ProjectChecks<LocalesOf<C>, Catalog, Routes, T>,
353
+ ProjectChecks<LocalesOf<C>, PublishedOf<Cat>, Routes, T>,
414
354
  >(
415
- // The first three are read for their types: they are how the locale union,
416
- // the message keys and the route ids are inferred without type arguments.
355
+ // Read for its type: it is how the locale union is inferred without type
356
+ // arguments.
417
357
  _config: C,
418
- _messages: Catalog,
358
+ catalog: Cat,
359
+ // Read for its type: the route ids.
419
360
  _routes: Routes,
420
- // Both validators sit in parameter position rather than in the constraint
421
- // above. They map the *whole* override object, and a whole-object mapped
422
- // type in a constraint makes one unrelated mistake fail the argument
423
- // outright — TypeScript then reports at this call instead of on the line
424
- // that is wrong.
425
- project: T & ProjectOverrideChecks<LocalesOf<C>, Catalog, Routes, T>
361
+ // The whole-object validators sit in parameter position rather than in the
362
+ // constraint above: in a constraint, one unrelated mistake fails the
363
+ // argument outright, and TypeScript reports at this call instead of on the
364
+ // line that is wrong.
365
+ project: T & ProjectOverrideChecks<PublishedOf<Cat>, Routes, T>
426
366
  ): T {
427
367
  // Checked at runtime as well as in the type, because the type is not always
428
368
  // what fails first. An Astro config is *evaluated* before `astro check`
429
- // runs, so a project file missing one of these reaches `mergeCatalog` and
430
- // throws there — a stack trace inside lib, naming a variable the reader
431
- // never wrote, for a field the compiler would have named precisely.
432
- //
433
- // Two lines to turn that back into the error it already was.
434
- for (const field of ["overrideMessages", "overrideRoutes"] as const) {
435
- if (project[field] === undefined) {
436
- throw new Error(
437
- `Project "${project.siteName}" declares no ${field}. It is required even when nothing is overridden — write \`${field}: {}\`, which is what the type asks for and what tells a reader the shared ${field === "overrideMessages" ? "copy" : "route table"} is used as it is.`
438
- );
439
- }
440
- }
441
- // The same two rules the type states, for the same reason: the config is
442
- // evaluated before the type check runs.
443
- for (const [key, entry] of Object.entries(project.addMessages ?? {})) {
444
- if (key in _messages) {
445
- throw new Error(
446
- `Project "${project.siteName}": addMessages "${key}" is already in the default catalog. Reword it in overrideMessages instead.`
447
- );
448
- }
449
- const missing = project.enabledLocales.filter(
450
- (locale) => typeof entry[locale] !== "string"
369
+ // runs, so a project file missing this reaches the route table and throws
370
+ // there, far from the field the compiler would have named precisely.
371
+ if (project.overrideRoutes === undefined) {
372
+ throw new Error(
373
+ `Project "${project.siteName}" declares no overrideRoutes. It is required even when nothing is overridden — write \`overrideRoutes: {}\`, which is what the type asks for and what tells a reader the shared route table is used as it is.`
451
374
  );
452
- if (missing.length > 0) {
453
- throw new Error(
454
- `Project "${project.siteName}": addMessages "${key}" has no ${missing.join(", ")}. A message a project adds is written for every locale it publishes.`
455
- );
456
- }
457
375
  }
376
+ const published: readonly string[] = catalog.locales;
458
377
  // The clock's rule, for the same reason.
459
378
  const clock = project.formats?.clock;
460
379
  if (clock !== undefined) {
461
- const published: readonly string[] = project.enabledLocales;
462
380
  const missing = published.filter((locale) => !(locale in clock));
463
381
  if (missing.length > 0) {
464
382
  throw new Error(
@@ -476,13 +394,11 @@ export function defineProject<
476
394
  }
477
395
  // The price locale's rule, for the same reason.
478
396
  const priceLocale: string | undefined = project.formats?.prices?.locale;
479
- if (
480
- priceLocale !== undefined &&
481
- !(project.enabledLocales as readonly string[]).includes(priceLocale)
482
- ) {
397
+ if (priceLocale !== undefined && !published.includes(priceLocale)) {
483
398
  throw new Error(
484
399
  `Project "${project.siteName}": formats.prices.locale names ${priceLocale}, which this project does not publish.`
485
400
  );
486
401
  }
402
+ builtWith.set(project, catalog);
487
403
  return project;
488
404
  }
@@ -15,7 +15,12 @@ import type { GeneratedFile } from "../file.ts";
15
15
  import type { PublicFilePath } from "../files.ts";
16
16
  import { mergeFonts } from "../fonts.ts";
17
17
  import type { HourCycle } from "../hours.ts";
18
- import { type BaseCatalog, mergeCatalog } from "../i18n/define.ts";
18
+ import type { MessagesOf, ProjectCatalog } from "../i18n/catalog.ts";
19
+ import {
20
+ type BaseCatalog,
21
+ mergeCatalog,
22
+ type OverrideCatalog,
23
+ } from "../i18n/define.ts";
19
24
  import { createTranslateFactory } from "../i18n/translate.ts";
20
25
  import type { ImageAsset } from "../image.ts";
21
26
  import { buildLlms, type LlmsItem, type LlmsSection } from "../llms.ts";
@@ -25,7 +30,11 @@ import {
25
30
  resolveShareImage,
26
31
  } from "../meta/index.ts";
27
32
  import type { PriceDecimals } from "../price-format.ts";
28
- import type { AddedCatalog, ProjectInput, ProjectRouteId } from "../project.ts";
33
+ import {
34
+ checkProjectCatalog,
35
+ type ProjectInput,
36
+ type ProjectRouteId,
37
+ } from "../project.ts";
29
38
  import {
30
39
  buildRedirects,
31
40
  type RedirectRule,
@@ -78,47 +87,52 @@ function decorate(path: UrlPath, options: LinkOptions | undefined): UrlPath {
78
87
  }
79
88
 
80
89
  /**
81
- * Turns a site config, its base data and one project overlay into the API the
82
- * pages use.
90
+ * Turns a site config, one project's catalog, the routes and the project into
91
+ * the API the pages use.
83
92
  *
84
93
  * This is the only wiring a consuming project writes: the merging, slug
85
94
  * fallbacks, URL building, translation and sitemap all come back ready to use.
86
95
  */
87
96
  export function createSite<
88
97
  const C extends SiteConfigShape,
89
- const Catalog extends BaseCatalog<LocalesOf<C>>,
90
- const Routes extends RouteRegistry<LocalesOf<C>>,
91
- // Inferred from `project.enabledLocales` as its own parameter rather than
92
- // read back as `P["enabledLocales"][number]`: that indexed access resolves
93
- // through the constraint's `readonly L[]` and collapses to `string`, which
94
- // silently un-types every locale the API accepts.
98
+ // The catalog carries the locales the project publishes and its copy.
99
+ const Cat extends ProjectCatalog<
100
+ BaseCatalog<LocalesOf<C>>,
101
+ LocalesOf<C>,
102
+ OverrideCatalog<LocalesOf<C>>,
103
+ OverrideCatalog<LocalesOf<C>>
104
+ >,
105
+ // Inferred from `catalog.locales` as its own parameter rather than read
106
+ // back off `Cat`: a type read back is not known to be one of the site's
107
+ // locales, where a parameter constrained to them is, and an indexed access
108
+ // through the constraint would collapse to the whole locale universe and
109
+ // silently un-type every locale the API accepts.
95
110
  const Published extends LocalesOf<C>,
111
+ const Routes extends RouteRegistry<LocalesOf<C>>,
96
112
  const P extends ProjectInput<LocalesOf<C>, Routes>,
97
113
  >(
98
114
  config_: C,
99
- messages: Catalog,
115
+ // `Published` is named in the *parameter*: a type parameter mentioned
116
+ // nowhere an argument reaches has nothing to infer from and silently falls
117
+ // back to its constraint.
118
+ catalog: Cat & { readonly locales: readonly Published[] },
100
119
  routes: Routes,
101
- // `Published` is named in the *parameter*, not only in `P`'s constraint:
102
- // a type parameter mentioned nowhere an argument reaches has nothing to
103
- // infer from and silently falls back to its constraint — here the whole
104
- // locale universe, which is the bug this was meant to fix.
105
- project: P & { readonly enabledLocales: readonly Published[] }
120
+ project: P
106
121
  ): Site<
107
122
  Published,
108
- Catalog & AddedCatalog<P>,
123
+ MessagesOf<Cat>,
109
124
  ProjectRouteId<Routes, P>,
110
125
  SectionPrefixOf<Routes, P["overrideRoutes"]>,
111
126
  OrphanSectionPrefixOf<Routes, P["overrideRoutes"]>
112
127
  > {
113
- // The locales this *project* publishes, not the universe the site config
114
- // declares. The runtime has always used the narrow set — `locales` below is
115
- // `project.enabledLocales` — but the type used to be the wide one, so
116
- // `pathFor("home", "el-GR")` type-checked on a build with no Greek pages and
117
- // returned a URL that was never generated.
128
+ // The locales this *project* publishes, from its catalog, not the
129
+ // universe the site config declares: `pathFor("home", "el-GR")` must not
130
+ // type-check on a build with no Greek pages, since it would return a URL
131
+ // that was never generated.
118
132
  type L = Published;
119
133
  type RouteId = ProjectRouteId<Routes, P>;
120
134
 
121
- const locales = project.enabledLocales as readonly L[];
135
+ const locales = catalog.locales as readonly L[];
122
136
  const defaultLocale = (project.overrideRouting?.defaultLocale ??
123
137
  config_.defaultRouting.defaultLocale) as L;
124
138
  const prefixDefaultLocale =
@@ -126,6 +140,7 @@ export function createSite<
126
140
  config_.defaultRouting.prefixDefaultLocale;
127
141
 
128
142
  // Checks that types cannot express, run once at build time.
143
+ checkProjectCatalog(project, catalog);
129
144
  if (locales.length === 0) {
130
145
  throw new Error("A project must publish at least one locale.");
131
146
  }
@@ -182,13 +197,13 @@ export function createSite<
182
197
  const llmsUrl: HttpsUrl | undefined =
183
198
  config_.llms === false ? undefined : joinUrl(siteUrl, `/${llmsName}`);
184
199
 
185
- // The project's own messages join the catalog here, typed and resolved like
200
+ // The project's own messages join the base here, typed and resolved like
186
201
  // any other: `Full` is what every reader below knows the keys by.
187
- type Full = Catalog & AddedCatalog<P>;
188
- const catalog = mergeCatalog<L>(
189
- messages,
190
- project.overrideMessages,
191
- project.addMessages
202
+ type Full = MessagesOf<Cat>;
203
+ const messages = mergeCatalog<L>(
204
+ catalog.base,
205
+ catalog.override,
206
+ catalog.add
192
207
  );
193
208
  const translateFactory = createTranslateFactory<Full, L>();
194
209
 
@@ -872,7 +887,7 @@ export function createSite<
872
887
  translate: <const Values extends string = never>(
873
888
  locale: L,
874
889
  bind?: { readonly values?: Readonly<Record<Values, string>> }
875
- ) => translateFactory<Values>(catalog, locale, bind?.values),
890
+ ) => translateFactory<Values>(messages, locale, bind?.values),
876
891
  rich: <
877
892
  const Links extends string = never,
878
893
  const Values extends string = never,
@@ -881,7 +896,7 @@ export function createSite<
881
896
  bind?: MessageBindings<RouteId, Links, Values>
882
897
  ) =>
883
898
  createRichText<Full, L, RouteId, Links, Values>({
884
- catalog,
899
+ catalog: messages,
885
900
  locale,
886
901
  link: richLink(locale),
887
902
  origin: siteUrl,
@@ -892,7 +907,7 @@ export function createSite<
892
907
  bind?: { readonly values?: Readonly<Record<Values, string>> }
893
908
  ) =>
894
909
  createPlainText<Full, L, Values>({
895
- catalog,
910
+ catalog: messages,
896
911
  locale,
897
912
  link: richLink(locale),
898
913
  origin: siteUrl,
@@ -4,11 +4,22 @@ import {
4
4
  type SiteConfigChecks,
5
5
  type SiteConfigShape,
6
6
  } from "../config.ts";
7
+ import {
8
+ type AddedEntries,
9
+ defineCatalog,
10
+ type OverrideChecks,
11
+ type OverrideEntries,
12
+ type ProjectCatalog,
13
+ type PublishedOf,
14
+ } from "../i18n/catalog.ts";
7
15
  import {
8
16
  type BaseCatalog,
9
17
  defineMessages,
10
18
  type NoExtraLocales,
19
+ type OverrideCatalog,
20
+ type ValidateAdditions,
11
21
  type ValidateBase,
22
+ type ValidateOverrideCatalog,
12
23
  } from "../i18n/define.ts";
13
24
  import {
14
25
  expandLanguages,
@@ -47,8 +58,9 @@ import { createSite } from "./create.ts";
47
58
  * const rooms = atlas.family("rooms", ROOMS, { enabled: true });
48
59
  * export const defaultRoutes = atlas.routes({ ...rooms, home: {…} });
49
60
  * export const defaultMessages = atlas.messages({…});
50
- * export const { project, redirects } = atlas.project(defaultMessages, defaultRoutes, {…});
51
- * export const site = atlas.site(defaultMessages, defaultRoutes, project);
61
+ * export const messages = atlas.catalog(defaultMessages, { locales: […], override: {…} });
62
+ * export const { project, redirects } = atlas.project(messages, defaultRoutes, {…});
63
+ * export const site = atlas.site(messages, defaultRoutes, project);
52
64
  * ```
53
65
  *
54
66
  * Every check each step makes is the same one it makes as a function: a typo'd
@@ -98,41 +110,74 @@ export function defineSite<
98
110
  return expandLanguages(Object.keys(config.locales) as L[], texts);
99
111
  },
100
112
 
101
- /** One deployment: what differs from the defaults. */
113
+ /**
114
+ * One deployment's copy: the locales it publishes, and what it rewords
115
+ * or adds. Built before the project, so the project's other files can
116
+ * name its messages by key: `CatalogKey<typeof catalog>`.
117
+ */
118
+ catalog<
119
+ const Base extends BaseCatalog<L>,
120
+ const Published extends L,
121
+ const Override extends OverrideEntries<Base, Published> &
122
+ OverrideChecks<Base, Published, Override>,
123
+ const Add extends AddedEntries<Published> = Record<never, never>,
124
+ >(
125
+ base: Base,
126
+ own: {
127
+ readonly locales: readonly Published[];
128
+ readonly override: Override &
129
+ ValidateOverrideCatalog<Override, Base, L>;
130
+ readonly add?: Add & ValidateAdditions<Add, Base, Published>;
131
+ }
132
+ ): ProjectCatalog<Base, Published, Override, Add> {
133
+ return defineCatalog(config, base, own as never) as never;
134
+ },
135
+
136
+ /** One deployment: what differs from the defaults, beside its catalog. */
102
137
  project<
103
- const Catalog extends BaseCatalog<L>,
138
+ const Cat extends ProjectCatalog<
139
+ BaseCatalog<L>,
140
+ L,
141
+ OverrideCatalog<L>,
142
+ OverrideCatalog<L>
143
+ >,
104
144
  const Routes extends RouteRegistry<L>,
105
145
  const T extends ProjectInput<L, Routes> &
106
- ProjectChecks<L, Catalog, Routes, T>,
146
+ ProjectChecks<L, PublishedOf<Cat>, Routes, T>,
107
147
  >(
108
- messages: Catalog,
148
+ catalog: Cat,
109
149
  routes: Routes,
110
- project: T & ProjectOverrideChecks<L, Catalog, Routes, T>
111
- ): DefinedProject<Routes, T> {
150
+ project: T & ProjectOverrideChecks<PublishedOf<Cat>, Routes, T>
151
+ ): DefinedProject<Routes, T, PublishedOf<Cat>> {
112
152
  const defined = defineProject(
113
153
  config,
114
- messages,
154
+ catalog,
115
155
  routes,
116
156
  project as never
117
157
  ) as T;
118
158
  return {
119
159
  project: defined,
120
- redirects: bindRedirects(routes, defined),
160
+ redirects: bindRedirects(routes, defined, catalog),
121
161
  };
122
162
  },
123
163
 
124
- /** The API every page reads, wired for one project. */
164
+ /** The API every page reads, wired for one project and its catalog. */
125
165
  site<
126
- const Catalog extends BaseCatalog<L>,
127
- const Routes extends RouteRegistry<L>,
166
+ const Cat extends ProjectCatalog<
167
+ BaseCatalog<L>,
168
+ L,
169
+ OverrideCatalog<L>,
170
+ OverrideCatalog<L>
171
+ >,
128
172
  const Published extends L,
173
+ const Routes extends RouteRegistry<L>,
129
174
  const P extends ProjectInput<L, Routes>,
130
175
  >(
131
- messages: Catalog,
176
+ catalog: Cat & { readonly locales: readonly Published[] },
132
177
  routes: Routes,
133
- project: P & { readonly enabledLocales: readonly Published[] }
134
- ): ReturnType<typeof createSite<C, Catalog, Routes, Published, P>> {
135
- return createSite(config, messages, routes, project);
178
+ project: P
179
+ ): ReturnType<typeof createSite<C, Cat, Published, Routes, P>> {
180
+ return createSite(config, catalog, routes, project);
136
181
  },
137
182
  };
138
183
  }