@escape-game-over/atlas 0.1.108 → 0.1.109

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
@@ -72,6 +72,7 @@ export const { project, redirects } = atlas.project(
72
72
 
73
73
  enabledLocales: ["el-GR", "en-US"],
74
74
  overrideRouting: { defaultLocale: "el-GR", prefixDefaultLocale: true },
75
+ clock: { "el-GR": "h23", "en-US": "h12" }, // how each language prints times
75
76
 
76
77
  overrideMessages: {
77
78
  // Greek only — the English copy still falls through to the default catalog.
@@ -243,6 +244,8 @@ regression-tested by an `@ts-expect-error` suite.
243
244
  | A `robots` directive that is not a real one | error — the vocabulary is a union |
244
245
  | A project `url` that is not `https://`, or ends in a slash | error |
245
246
  | `defaultLocale` not among the project's `enabledLocales` | error |
247
+ | A `clock` leaving out a published locale, or naming an unpublished one | error |
248
+ | `site.clockFor` on a project that declares no clock | throws, naming the project |
246
249
  | An unknown field in a project declaration | error — a typo cannot sit there unread |
247
250
  | Two routes resolving to the same URL | build throws, naming both |
248
251
  | A site icon that is not square, a multiple of 48px, and PNG | build throws |
package/docs/checks.md CHANGED
@@ -42,6 +42,7 @@ sees.
42
42
  | unknown message key, or an override dropping a `{placeholder}` | two routes resolving to the same URL |
43
43
  | a message the project declared mandatory and never wrote (`CatalogCovers`) | |
44
44
  | copy or a locale label written for a locale outside `enabledLocales` | |
45
+ | a `clock` leaving out a published locale, or naming one outside `enabledLocales` | the same, for a project that was not type-checked first |
45
46
  | 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) |
46
47
  | unknown route id, or a locale the site does not ship | a locale published twice |
47
48
  | a malformed language tag (`en_US`, `en-us`, `EN-US`, bare `en`) | an icon that is not square, a multiple of 48, and PNG |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@escape-game-over/atlas",
3
- "version": "0.1.108",
3
+ "version": "0.1.109",
4
4
  "type": "module",
5
5
  "description": "Typed, data-driven machinery for static multi-locale, multi-deployment Astro sites.",
6
6
  "private": false,
@@ -63,10 +63,10 @@
63
63
  "vite": ">= 8"
64
64
  },
65
65
  "devDependencies": {
66
- "@biomejs/biome": "2.6.6",
66
+ "@biomejs/biome": "2.6.7",
67
67
  "@types/node": "26.6.6",
68
68
  "@vitest/coverage-istanbul": "5.1.0",
69
- "astro": "7.4.10",
69
+ "astro": "7.4.11",
70
70
  "typescript": "6.0.3",
71
71
  "vitest": "5.1.0"
72
72
  }
package/src/project.ts CHANGED
@@ -7,6 +7,7 @@ import type {
7
7
  SiteConfigShape,
8
8
  } from "./config.ts";
9
9
  import type { Fonts } from "./fonts.ts";
10
+ import type { HourCycle } from "./hours.ts";
10
11
  import type {
11
12
  BaseCatalog,
12
13
  OverrideCatalog,
@@ -105,6 +106,21 @@ export interface ProjectInput<L extends string, Routes> {
105
106
  * declare is a compile error.
106
107
  */
107
108
  readonly enabledLocales: readonly L[];
109
+ /**
110
+ * The clock each published language prints times on: `"h23"` for 18:00,
111
+ * `"h12"` for 6:00 PM. Read back with `site.clockFor(locale)`.
112
+ *
113
+ * One entry for every locale in `enabledLocales`, and none for any other,
114
+ * so what each page prints is written down beside the languages that print
115
+ * it: a bilingual Canadian site reads `{ "en-CA": "h12", "fr-CA": "h23" }`.
116
+ * Not taken from the locale's own default (`Intl`'s) on purpose: a site may
117
+ * print a 24-hour clock where its language's default is 12-hour, and that
118
+ * choice is the site's to state.
119
+ *
120
+ * Optional: a project that prints no times declares none, and
121
+ * `clockFor` refuses to answer for it.
122
+ */
123
+ readonly clock?: Readonly<Partial<Record<L, HourCycle>>>;
108
124
  /**
109
125
  * Replaces parts of the site's `defaultRouting` for this project.
110
126
  *
@@ -286,6 +302,17 @@ export type ProjectChecks<
286
302
  ? NoExcessKeys<T["overrideLocaleMeta"][K], keyof LocaleMetaOverride>
287
303
  : LocaleNotEnabled<K>;
288
304
  };
305
+ // The locales written must be ones this project publishes, as for
306
+ // `overrideLocaleMeta` above. That every one it publishes is there is
307
+ // checked in `ProjectOverrideChecks`: a conditional on `T` here would make
308
+ // the constraint circular.
309
+ readonly clock?: {
310
+ readonly [K in StringKeys<
311
+ T["clock"]
312
+ >]: K extends T["enabledLocales"][number]
313
+ ? HourCycle
314
+ : LocaleNotEnabled<K>;
315
+ };
289
316
  };
290
317
 
291
318
  /** The same checks that map the whole override objects, in parameter position. */
@@ -312,7 +339,16 @@ export type ProjectOverrideChecks<
312
339
  T["enabledLocales"][number]
313
340
  >;
314
341
  }
315
- : unknown);
342
+ : unknown) &
343
+ (T extends { readonly clock: object }
344
+ ? {
345
+ // A clock, once declared, is declared for every page: a locale
346
+ // left out would have no clock to print its times on.
347
+ readonly clock: {
348
+ readonly [K in T["enabledLocales"][number]]: HourCycle;
349
+ };
350
+ }
351
+ : unknown);
316
352
 
317
353
  /** A project's own messages, as a catalog: none when it adds none. */
318
354
  export type AddedCatalog<P> = P extends { readonly addMessages: infer A }
@@ -369,5 +405,25 @@ export function defineProject<
369
405
  );
370
406
  }
371
407
  }
408
+ // The clock's rule, for the same reason.
409
+ if (project.clock !== undefined) {
410
+ const published: readonly string[] = project.enabledLocales;
411
+ const missing = published.filter(
412
+ (locale) => !(locale in (project.clock ?? {}))
413
+ );
414
+ if (missing.length > 0) {
415
+ throw new Error(
416
+ `Project "${project.siteName}": clock has no ${missing.join(", ")}. A project that declares a clock gives one for every locale it publishes.`
417
+ );
418
+ }
419
+ const extra = Object.keys(project.clock).filter(
420
+ (locale) => !published.includes(locale)
421
+ );
422
+ if (extra.length > 0) {
423
+ throw new Error(
424
+ `Project "${project.siteName}": clock names ${extra.join(", ")}, which this project does not publish.`
425
+ );
426
+ }
427
+ }
372
428
  return project;
373
429
  }
package/src/site/api.ts CHANGED
@@ -7,6 +7,7 @@ import type {
7
7
  import type { GeneratedFile } from "../file.ts";
8
8
  import type { PublicFilePath } from "../files.ts";
9
9
  import type { Fonts } from "../fonts.ts";
10
+ import type { HourCycle } from "../hours.ts";
10
11
  import type { TranslateFor } from "../i18n/translate.ts";
11
12
  import type { LlmsSection } from "../llms.ts";
12
13
  import type { ThemeColor } from "../meta/index.ts";
@@ -219,6 +220,15 @@ export interface Site<
219
220
  bind?: { readonly values?: Readonly<Record<Values, string>> }
220
221
  ): PlainTextFor<Catalog, Values>;
221
222
 
223
+ /**
224
+ * The clock a locale's pages print times on, as the project's `clock`
225
+ * states it: hand it to `formatHours`, or to anything formatting a time.
226
+ *
227
+ * Throws for a project that declares no clock: one that prints times has
228
+ * to say how.
229
+ */
230
+ clockFor(locale: L): HourCycle;
231
+
222
232
  /** Absolute path of a route in one locale, e.g. `/el/sxetika-me-emas`. */
223
233
  pathFor(id: RouteId, locale: L, options?: LinkOptions): UrlPath;
224
234
  /** The same, prefixed with the site origin — for canonicals and sitemaps. */
@@ -14,6 +14,7 @@ import {
14
14
  import type { GeneratedFile } from "../file.ts";
15
15
  import type { PublicFilePath } from "../files.ts";
16
16
  import { mergeFonts } from "../fonts.ts";
17
+ import type { HourCycle } from "../hours.ts";
17
18
  import { type BaseCatalog, mergeCatalog } from "../i18n/define.ts";
18
19
  import { createTranslateFactory } from "../i18n/translate.ts";
19
20
  import type { ImageAsset } from "../image.ts";
@@ -192,6 +193,18 @@ export function createSite<
192
193
 
193
194
  const resolved = mergeRoutes<L, Routes>(routes, project.overrideRoutes);
194
195
 
196
+ function clockFor(locale: L): HourCycle {
197
+ const cycle = project.clock?.[locale];
198
+ if (cycle === undefined) {
199
+ throw new Error(
200
+ project.clock === undefined
201
+ ? `Project "${project.siteName}" declares no clock, so it cannot print a time. Add \`clock\`, with an hour cycle for each locale it publishes.`
202
+ : `Project "${project.siteName}": clock has no ${locale}.`
203
+ );
204
+ }
205
+ return cycle;
206
+ }
207
+
195
208
  function pathFor(id: RouteId, locale: L, options?: LinkOptions): UrlPath {
196
209
  const route = resolved[id];
197
210
  if (route === undefined) {
@@ -867,6 +880,7 @@ export function createSite<
867
880
  ...bind,
868
881
  }),
869
882
 
883
+ clockFor,
870
884
  pathFor,
871
885
  urlFor,
872
886
  fileUrl,