@escape-game-over/atlas 0.1.99 → 0.1.101

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@escape-game-over/atlas",
3
- "version": "0.1.99",
3
+ "version": "0.1.101",
4
4
  "type": "module",
5
5
  "description": "Typed, data-driven machinery for static multi-locale, multi-deployment Astro sites.",
6
6
  "private": false,
@@ -10,8 +10,11 @@
10
10
  */
11
11
 
12
12
  export {
13
+ formatTime,
14
+ type HourCycle,
13
15
  type OpenStatus,
14
16
  openStatus,
17
+ type TimeFormat,
15
18
  type VenueNow,
16
19
  } from "../hours.ts";
17
20
  export * from "./attribution.ts";
@@ -28,8 +28,10 @@ export type ContactFormType =
28
28
  | "birthday_party"
29
29
  | "board_games"
30
30
  | "group_booking"
31
+ | "school_trips"
31
32
  | "team_building"
32
- | "voucher";
33
+ | "voucher"
34
+ | "vr_games";
33
35
 
34
36
  /** Where a deployment's contact mail goes. */
35
37
  export interface MailEndpoint {
package/src/hours.ts CHANGED
@@ -164,6 +164,13 @@ export function groupHours(
164
164
  return [...groups.values()];
165
165
  }
166
166
 
167
+ /**
168
+ * Which clock times print on: `"h23"` as written (`14:00`), `"h12"` on a
169
+ * 12-hour clock (`2:00 PM`). Never the locale's default, which gives Greek a
170
+ * 12-hour clock nobody there uses on a door.
171
+ */
172
+ export type HourCycle = "h12" | "h23";
173
+
167
174
  /**
168
175
  * How to print a week. Day names come from `Intl` in `locale`; "closed" is
169
176
  * copy, so the caller translates it.
@@ -175,12 +182,8 @@ export interface HoursFormat {
175
182
  weekday?: "short" | "long";
176
183
  /** The word for a day the venue is shut. */
177
184
  closed: string;
178
- /**
179
- * Times print as written (`14:00`) unless this is `"h12"` (`2:00 PM`).
180
- * Not the locale's default, which gives Greek a 12-hour clock nobody there
181
- * uses on a door.
182
- */
183
- hourCycle?: "h12";
185
+ /** As written (`"h23"`) when absent. */
186
+ hourCycle?: HourCycle;
184
187
  /** Between the ends of a span: `Mon–Thu`, `14:00–23:30`. */
185
188
  between?: string;
186
189
  /** Between spans: `Mon–Thu, Sun`. */
@@ -226,17 +229,41 @@ export function formatHours(
226
229
  }));
227
230
  }
228
231
 
232
+ /** How to say a week in a sentence: `HoursFormat` without its separators. */
233
+ export type HoursSentenceFormat = Omit<HoursFormat, "and">;
234
+
229
235
  /**
230
- * How to say a week in a sentence: `HoursFormat` without the separators it
231
- * picks from `Intl`, plus the one `Intl` has no word for.
236
+ * Each language's semicolon, between one group of days and the next. `Intl`
237
+ * has no list form for it, and it is punctuation rather than copy, so it lives
238
+ * here. A language missing from this table throws rather than borrowing the
239
+ * English one: add it here.
232
240
  */
233
- export interface HoursSentenceFormat extends Omit<HoursFormat, "and"> {
234
- /**
235
- * Between one group of days and the next: `"; "` in English, `"؛ "` in
236
- * Arabic. Copy, so the caller translates it; `Intl` has no list form
237
- * for it.
238
- */
239
- readonly separator: string;
241
+ const SEMICOLONS: Readonly<Record<string, string>> = {
242
+ ar: "؛ ",
243
+ de: "; ",
244
+ el: "· ",
245
+ en: "; ",
246
+ es: "; ",
247
+ // A narrow no-break space before it, as French sets it.
248
+ fr: "\u202F; ",
249
+ hu: "; ",
250
+ it: "; ",
251
+ pt: "; ",
252
+ ru: "; ",
253
+ sv: "; ",
254
+ };
255
+
256
+ function semicolonFor(locale: string): string {
257
+ const { language } = new Intl.Locale(locale);
258
+ const semicolon = Object.hasOwn(SEMICOLONS, language)
259
+ ? SEMICOLONS[language]
260
+ : undefined;
261
+ if (semicolon === undefined) {
262
+ throw new Error(
263
+ `hoursSentence has no semicolon for "${language}" (${locale}). Add it to SEMICOLONS in hours.ts.`
264
+ );
265
+ }
266
+ return semicolon;
240
267
  }
241
268
 
242
269
  /**
@@ -245,13 +272,15 @@ export interface HoursSentenceFormat extends Omit<HoursFormat, "and"> {
245
272
  *
246
273
  * Days and times join with the locale's own "and" (`Intl.ListFormat`), so a
247
274
  * sentence in Arabic reads `الاثنين–الخميس والأحد` rather than borrowing an
248
- * English comma. No closing full stop: the copy around it decides that.
275
+ * English comma; groups with the language's semicolon (`؛` in Arabic). No
276
+ * closing full stop: the copy around it decides that.
249
277
  */
250
278
  export function hoursSentence(
251
279
  hours: WeeklyHours,
252
280
  format: HoursSentenceFormat
253
281
  ): string {
254
282
  const between = format.between ?? "–";
283
+ const semicolon = semicolonFor(format.locale);
255
284
  const { dayName, time } = printer(format);
256
285
  const list = new Intl.ListFormat(format.locale, {
257
286
  type: "conjunction",
@@ -275,7 +304,34 @@ export function hoursSentence(
275
304
  );
276
305
  return `${days} ${times}`;
277
306
  })
278
- .join(format.separator);
307
+ .join(semicolon);
308
+ }
309
+
310
+ /** How `formatTime` prints a time. */
311
+ export interface TimeFormat {
312
+ /** The language the clock is read in, e.g. `"en-US"`. */
313
+ readonly locale: string;
314
+ /** As written (`"h23"`) when absent. */
315
+ readonly hourCycle?: HourCycle;
316
+ }
317
+
318
+ /**
319
+ * One time as the hours print it: as written (`14:00`), or on a 12-hour clock
320
+ * (`2:00 PM`). For a line that quotes a single time, such as "open until",
321
+ * so it reads like the table beside it.
322
+ */
323
+ export function formatTime(at: TimeOfDay, format: TimeFormat): string {
324
+ if (format.hourCycle !== "h12") return at;
325
+ return new Intl.DateTimeFormat(format.locale, {
326
+ hour: "numeric",
327
+ minute: "2-digit",
328
+ hourCycle: "h12",
329
+ timeZone: "UTC",
330
+ }).format(
331
+ new Date(
332
+ Date.UTC(2024, 0, 1, Number(at.slice(0, 2)), Number(at.slice(3, 5)))
333
+ )
334
+ );
279
335
  }
280
336
 
281
337
  /** Day names and times as `format` prints them. */
@@ -289,29 +345,7 @@ function printer(
289
345
  });
290
346
  const dayName = (day: Weekday): string =>
291
347
  names.format(new Date(Date.UTC(2024, 0, 1 + WEEK.indexOf(day))));
292
- const clock =
293
- format.hourCycle === "h12"
294
- ? new Intl.DateTimeFormat(format.locale, {
295
- hour: "numeric",
296
- minute: "2-digit",
297
- hourCycle: "h12",
298
- timeZone: "UTC",
299
- })
300
- : undefined;
301
- const time = (at: TimeOfDay): string =>
302
- clock === undefined
303
- ? at
304
- : clock.format(
305
- new Date(
306
- Date.UTC(
307
- 2024,
308
- 0,
309
- 1,
310
- Number(at.slice(0, 2)),
311
- Number(at.slice(3, 5))
312
- )
313
- )
314
- );
348
+ const time = (at: TimeOfDay): string => formatTime(at, format);
315
349
  return { dayName, time };
316
350
  }
317
351
 
package/src/index.ts CHANGED
@@ -120,13 +120,16 @@ export {
120
120
  type DayHours,
121
121
  type FormattedHours,
122
122
  formatHours,
123
+ formatTime,
123
124
  groupHours,
125
+ type HourCycle,
124
126
  type HoursFormat,
125
127
  type HoursGroup,
126
128
  type HoursSentenceFormat,
127
129
  hoursSentence,
128
130
  type OpenStatus,
129
131
  openStatus,
132
+ type TimeFormat,
130
133
  type TimeOfDay,
131
134
  type TimeRange,
132
135
  type VenueNow,