@jtakeit/kit 0.3.0 → 0.5.1

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.
@@ -14,6 +14,31 @@ export const LOCALES = Object.freeze(['uk', 'de', 'en', 'ru', 'es', 'it', 'pt',
14
14
 
15
15
  const BASE = {
16
16
  uk: {
17
+ // the form's look — the steps, the week, the parts of the day, the
18
+ // kept time, the link kept (10 October 2026)
19
+ notWith: 'не з {who}',
20
+ retime: 'Візит тепер іншої тривалості, тому час змінився — оберіть ще раз.',
21
+ until: 'Приблизно до {time}',
22
+ withNamed: 'Майстер: {who}',
23
+ keep: 'Збережіть це посилання — ним можна перенести чи скасувати запис.',
24
+ copy: 'Копіювати',
25
+ copied: 'Скопійовано',
26
+ open: 'Відкрити',
27
+ stepWhat: 'Що і коли',
28
+ stepWho: 'Хто прийде',
29
+ next: 'Далі',
30
+ change: 'Змінити',
31
+ needAll: 'Спершу оберіть послугу, день і час.',
32
+ summary: 'Ваш запис',
33
+ what: 'Що',
34
+ when: 'Коли',
35
+ takes: 'Тривалість',
36
+ morning: 'Ранок',
37
+ afternoon: 'День',
38
+ evening: 'Вечір',
39
+ prevWeek: 'Попередній тиждень',
40
+ nextWeek: 'Наступний тиждень',
41
+ again: 'Записатися ще раз',
17
42
  service: 'Послуга', services: 'Послуги', total: 'Разом', pickService: 'Оберіть хоча б одну послугу.', resource: 'Майстер', any: 'Будь-хто', day: 'День', time: 'Час',
18
43
  name: "Ім'я", phone: 'Телефон', email: 'Електронна пошта', note: 'Коментар',
19
44
  book: 'Записатися', sending: 'Записуємо…', none: 'У цей день вільного часу немає.', noParty: 'Немає вільних місць для такої компанії.',
@@ -27,6 +52,31 @@ const BASE = {
27
52
  needContact: 'Вкажіть телефон або пошту, щоб ми могли з вами звʼязатися.', noScript: 'Онлайн-форма працює з увімкненим JavaScript — увімкніть його або зателефонуйте нам.',
28
53
  },
29
54
  de: {
55
+ // the form's look — the steps, the week, the parts of the day, the
56
+ // kept time, the link kept (10 October 2026)
57
+ notWith: 'nicht bei {who}',
58
+ retime: 'Der Besuch dauert jetzt anders lang, darum haben sich die Zeiten verschoben — bitte wählen Sie neu.',
59
+ until: 'Bis etwa {time}',
60
+ withNamed: 'Bei {who}',
61
+ keep: 'Bewahren Sie diesen Link auf — damit verschieben oder stornieren Sie den Termin.',
62
+ copy: 'Kopieren',
63
+ copied: 'Kopiert',
64
+ open: 'Öffnen',
65
+ stepWhat: 'Was und wann',
66
+ stepWho: 'Wer kommt',
67
+ next: 'Weiter',
68
+ change: 'Ändern',
69
+ needAll: 'Bitte zuerst Leistung, Tag und Zeit wählen.',
70
+ summary: 'Ihr Termin',
71
+ what: 'Was',
72
+ when: 'Wann',
73
+ takes: 'Dauer',
74
+ morning: 'Vormittag',
75
+ afternoon: 'Nachmittag',
76
+ evening: 'Abend',
77
+ prevWeek: 'Vorherige Woche',
78
+ nextWeek: 'Nächste Woche',
79
+ again: 'Noch einen Termin buchen',
30
80
  service: 'Leistung', services: 'Leistungen', total: 'Zusammen', pickService: 'Bitte mindestens eine Leistung wählen.', resource: 'Bei', any: 'Egal wer', day: 'Tag', time: 'Uhrzeit',
31
81
  name: 'Name', phone: 'Telefon', email: 'E-Mail', note: 'Bemerkung',
32
82
  book: 'Termin buchen', sending: 'Wird gebucht…', none: 'An diesem Tag ist nichts frei.', noParty: 'Für so viele Personen gibt es keinen freien Platz.',
@@ -40,6 +90,31 @@ const BASE = {
40
90
  needContact: 'Bitte Telefon oder E-Mail angeben, damit wir Sie erreichen können.', noScript: 'Für die Online-Buchung ist JavaScript nötig — oder rufen Sie uns an.',
41
91
  },
42
92
  en: {
93
+ // the form's look — the steps, the week, the parts of the day, the
94
+ // kept time, the link kept (10 October 2026)
95
+ notWith: 'not with {who}',
96
+ retime: 'The visit is a different length now, so the times moved — pick one again.',
97
+ until: 'Until about {time}',
98
+ withNamed: 'With {who}',
99
+ keep: 'Keep this link — it is how you move or cancel the booking.',
100
+ copy: 'Copy',
101
+ copied: 'Copied',
102
+ open: 'Open',
103
+ stepWhat: 'What and when',
104
+ stepWho: 'Who is coming',
105
+ next: 'Continue',
106
+ change: 'Change',
107
+ needAll: 'Pick a service, a day and a time first.',
108
+ summary: 'Your booking',
109
+ what: 'What',
110
+ when: 'When',
111
+ takes: 'Takes',
112
+ morning: 'Morning',
113
+ afternoon: 'Afternoon',
114
+ evening: 'Evening',
115
+ prevWeek: 'Previous week',
116
+ nextWeek: 'Next week',
117
+ again: 'Book another time',
43
118
  service: 'Service', services: 'Services', total: 'Total', pickService: 'Pick at least one service.', resource: 'With', any: 'Anyone', day: 'Day', time: 'Time',
44
119
  name: 'Name', phone: 'Phone', email: 'Email', note: 'Note',
45
120
  book: 'Book', sending: 'Booking…', none: 'Nothing is free on that day.', noParty: 'There is no place for a party this large.',
@@ -53,6 +128,31 @@ const BASE = {
53
128
  needContact: 'Leave a phone number or an email so we can reach you.', noScript: 'Booking online needs JavaScript — or ring us.',
54
129
  },
55
130
  ru: {
131
+ // the form's look — the steps, the week, the parts of the day, the
132
+ // kept time, the link kept (10 October 2026)
133
+ notWith: 'не у {who}',
134
+ retime: 'Визит теперь другой длительности, поэтому время изменилось — выберите ещё раз.',
135
+ until: 'Примерно до {time}',
136
+ withNamed: 'Мастер: {who}',
137
+ keep: 'Сохраните эту ссылку — по ней можно перенести или отменить запись.',
138
+ copy: 'Копировать',
139
+ copied: 'Скопировано',
140
+ open: 'Открыть',
141
+ stepWhat: 'Что и когда',
142
+ stepWho: 'Кто придёт',
143
+ next: 'Далее',
144
+ change: 'Изменить',
145
+ needAll: 'Сначала выберите услугу, день и время.',
146
+ summary: 'Ваша запись',
147
+ what: 'Что',
148
+ when: 'Когда',
149
+ takes: 'Длительность',
150
+ morning: 'Утро',
151
+ afternoon: 'День',
152
+ evening: 'Вечер',
153
+ prevWeek: 'Предыдущая неделя',
154
+ nextWeek: 'Следующая неделя',
155
+ again: 'Записаться ещё раз',
56
156
  service: "Услуга",
57
157
  services: "Услуги",
58
158
  total: "Итого",
@@ -96,6 +196,31 @@ const BASE = {
96
196
  noScript: "Онлайн-форма работает с включенным JavaScript — включите его или позвоните нам.",
97
197
  },
98
198
  es: {
199
+ // the form's look — the steps, the week, the parts of the day, the
200
+ // kept time, the link kept (10 October 2026)
201
+ notWith: 'no con {who}',
202
+ retime: 'La visita ahora dura distinto, así que los horarios cambiaron: elija uno de nuevo.',
203
+ until: 'Hasta las {time} aproximadamente',
204
+ withNamed: 'Con {who}',
205
+ keep: 'Guarde este enlace: con él puede mover o cancelar la reserva.',
206
+ copy: 'Copiar',
207
+ copied: 'Copiado',
208
+ open: 'Abrir',
209
+ stepWhat: 'Qué y cuándo',
210
+ stepWho: 'Quién viene',
211
+ next: 'Continuar',
212
+ change: 'Cambiar',
213
+ needAll: 'Elija primero un servicio, un día y una hora.',
214
+ summary: 'Su reserva',
215
+ what: 'Qué',
216
+ when: 'Cuándo',
217
+ takes: 'Duración',
218
+ morning: 'Mañana',
219
+ afternoon: 'Tarde',
220
+ evening: 'Noche',
221
+ prevWeek: 'Semana anterior',
222
+ nextWeek: 'Semana siguiente',
223
+ again: 'Reservar otra hora',
99
224
  service: "Servicio",
100
225
  services: "Servicios",
101
226
  total: "Total",
@@ -139,6 +264,31 @@ const BASE = {
139
264
  noScript: "Para reservar en línea hace falta JavaScript — o llámenos.",
140
265
  },
141
266
  it: {
267
+ // the form's look — the steps, the week, the parts of the day, the
268
+ // kept time, the link kept (10 October 2026)
269
+ notWith: 'non con {who}',
270
+ retime: 'La visita ora ha una durata diversa, quindi gli orari sono cambiati: ne scelga uno di nuovo.',
271
+ until: 'Fino alle {time} circa',
272
+ withNamed: 'Con {who}',
273
+ keep: 'Conservi questo link: serve per spostare o annullare la prenotazione.',
274
+ copy: 'Copia',
275
+ copied: 'Copiato',
276
+ open: 'Apri',
277
+ stepWhat: 'Cosa e quando',
278
+ stepWho: 'Chi viene',
279
+ next: 'Continua',
280
+ change: 'Cambia',
281
+ needAll: 'Scelga prima un servizio, un giorno e un orario.',
282
+ summary: 'La sua prenotazione',
283
+ what: 'Cosa',
284
+ when: 'Quando',
285
+ takes: 'Durata',
286
+ morning: 'Mattina',
287
+ afternoon: 'Pomeriggio',
288
+ evening: 'Sera',
289
+ prevWeek: 'Settimana precedente',
290
+ nextWeek: 'Settimana successiva',
291
+ again: 'Prenota un altro orario',
142
292
  service: "Servizio",
143
293
  services: "Servizi",
144
294
  total: "Totale",
@@ -182,6 +332,31 @@ const BASE = {
182
332
  noScript: "Per prenotare online serve JavaScript: oppure ci chiami.",
183
333
  },
184
334
  pt: {
335
+ // the form's look — the steps, the week, the parts of the day, the
336
+ // kept time, the link kept (10 October 2026)
337
+ notWith: 'não com {who}',
338
+ retime: 'A visita tem agora outra duração, por isso os horários mudaram — escolha de novo.',
339
+ until: 'Até cerca das {time}',
340
+ withNamed: 'Com {who}',
341
+ keep: 'Guarde esta ligação — é com ela que move ou cancela a marcação.',
342
+ copy: 'Copiar',
343
+ copied: 'Copiado',
344
+ open: 'Abrir',
345
+ stepWhat: 'O quê e quando',
346
+ stepWho: 'Quem vem',
347
+ next: 'Continuar',
348
+ change: 'Alterar',
349
+ needAll: 'Escolha primeiro um serviço, um dia e uma hora.',
350
+ summary: 'A sua marcação',
351
+ what: 'O quê',
352
+ when: 'Quando',
353
+ takes: 'Duração',
354
+ morning: 'Manhã',
355
+ afternoon: 'Tarde',
356
+ evening: 'Noite',
357
+ prevWeek: 'Semana anterior',
358
+ nextWeek: 'Semana seguinte',
359
+ again: 'Marcar outra hora',
185
360
  service: "Serviço",
186
361
  services: "Serviços",
187
362
  total: "Total",
@@ -225,6 +400,31 @@ const BASE = {
225
400
  noScript: "Reservar online precisa de JavaScript — ou ligue-nos.",
226
401
  },
227
402
  fr: {
403
+ // the form's look — the steps, the week, the parts of the day, the
404
+ // kept time, the link kept (10 October 2026)
405
+ notWith: 'pas avec {who}',
406
+ retime: 'La visite n’a plus la même durée, les horaires ont donc changé — choisissez-en un à nouveau.',
407
+ until: 'Jusqu’à {time} environ',
408
+ withNamed: 'Avec {who}',
409
+ keep: 'Conservez ce lien : c’est avec lui que vous déplacez ou annulez la réservation.',
410
+ copy: 'Copier',
411
+ copied: 'Copié',
412
+ open: 'Ouvrir',
413
+ stepWhat: 'Quoi et quand',
414
+ stepWho: 'Qui vient',
415
+ next: 'Continuer',
416
+ change: 'Modifier',
417
+ needAll: 'Choisissez d’abord une prestation, un jour et une heure.',
418
+ summary: 'Votre réservation',
419
+ what: 'Quoi',
420
+ when: 'Quand',
421
+ takes: 'Durée',
422
+ morning: 'Matin',
423
+ afternoon: 'Après-midi',
424
+ evening: 'Soir',
425
+ prevWeek: 'Semaine précédente',
426
+ nextWeek: 'Semaine suivante',
427
+ again: 'Réserver un autre créneau',
228
428
  service: "Prestation",
229
429
  services: "Prestations",
230
430
  total: "Total",
package/lib/catalogue.mjs CHANGED
@@ -1346,13 +1346,28 @@ const sharedBlocks = existsSync(sharedFile)
1346
1346
  const byType = new Map(catalogue.blocks.map((block) => [block.type, block]));
1347
1347
  const disagreements = [];
1348
1348
 
1349
+ /** `/de/services/x` without its language: `/services/x`. The site's own language has no prefix. */
1350
+ function basePathOf(path, locales) {
1351
+ for (const locale of locales) {
1352
+ if (path === `/${locale}` || path === `/${locale}/`) return '/';
1353
+ if (path.startsWith(`/${locale}/`)) return path.slice(locale.length + 1);
1354
+ }
1355
+ return path;
1356
+ }
1357
+
1349
1358
  for (const page of pages) {
1350
1359
  const onThePage = annotationsByPage.get(page.path);
1351
1360
  if (onThePage === undefined) {
1352
1361
  // A collection with `pages: false` builds no page for its entries: they
1353
1362
  // are read by the page that lists them and by the diary.
1363
+ // Said by the entry's own collection, or by its address — with the
1364
+ // language in front of it for an entry in another one: `/de/services/x`
1365
+ // is the `services` collection's as much as `/services/x` is (wiki/75 of
1366
+ // the platform; a German salon's eight services were each reported as a
1367
+ // page the build did not produce, 10 October 2026).
1368
+ const bare = basePathOf(page.path, catalogue.locales ?? []);
1354
1369
  const listedOnly = collections.some(
1355
- (c) => c.pages === false && page.path.startsWith(`${c.prefix}/`),
1370
+ (c) => c.pages === false && (page.collection === c.name || bare.startsWith(`${c.prefix}/`)),
1356
1371
  );
1357
1372
  if (listedOnly) {
1358
1373
  /*
@@ -1502,8 +1517,9 @@ for (const [where, found] of annotationsByPage) {
1502
1517
  * against the entry's own type.
1503
1518
  */
1504
1519
  const entry = pages.find((one) => one.path === address);
1520
+ // By the entry's collection, or its address without the language.
1505
1521
  const listedOnly = entry !== undefined && collections.some(
1506
- (c) => c.pages === false && address.startsWith(`${c.prefix}/`),
1522
+ (c) => c.pages === false && (entry.collection === c.name || basePathOf(address, catalogue.locales ?? []).startsWith(`${c.prefix}/`)),
1507
1523
  );
1508
1524
  if (listedOnly) {
1509
1525
  const m = /^blocks\[(\d+)\]\.([A-Za-z0-9_]+)/.exec(inner);
package/lib/money.mjs CHANGED
@@ -15,16 +15,36 @@
15
15
  * @returns {string}
16
16
  */
17
17
  export function money(minor, currency, locale) {
18
- const value = minor / 100;
19
- const digits = Number.isInteger(value) ? 0 : 2;
18
+ // In the currency's own minor unit (11 October 2026): a hundredth of a
19
+ // franc, a whole yen, a thousandth of a dinar — ISO 4217's digits, which
20
+ // `Intl` carries and the platform counts by too. A hundredth everywhere
21
+ // showed ¥1000 as «¥10».
22
+ const places = currency ? digitsOf(currency, locale) : 2;
23
+ const value = minor / 10 ** places;
24
+ const digits = Number.isInteger(value) ? 0 : places;
20
25
  if (!currency) {
21
- return new Intl.NumberFormat(locale, { minimumFractionDigits: digits, maximumFractionDigits: 2 }).format(value);
26
+ return new Intl.NumberFormat(locale, { minimumFractionDigits: digits, maximumFractionDigits: places }).format(value);
22
27
  }
23
28
  return new Intl.NumberFormat(locale, {
24
29
  style: 'currency',
25
30
  currency: currency.toUpperCase(),
26
31
  currencyDisplay: 'narrowSymbol',
27
32
  minimumFractionDigits: digits,
28
- maximumFractionDigits: 2,
33
+ maximumFractionDigits: places,
29
34
  }).format(value);
30
35
  }
36
+
37
+ /**
38
+ * How many digits a currency's minor unit has, by ISO 4217 as `Intl` knows it.
39
+ *
40
+ * @param {string} currency
41
+ * @param {string} [locale]
42
+ * @returns {number}
43
+ */
44
+ export function digitsOf(currency, locale = 'en') {
45
+ try {
46
+ return new Intl.NumberFormat(locale, { style: 'currency', currency: currency.toUpperCase() }).resolvedOptions().maximumFractionDigits ?? 2;
47
+ } catch {
48
+ return 2;
49
+ }
50
+ }
package/lib/phases.mjs ADDED
@@ -0,0 +1,139 @@
1
+ /**
2
+ * The seven phases of a site being written, from two halves (jtakeit-core,
3
+ * wiki/73 · §4 and §7).
4
+ *
5
+ * The kit's half is `readProgress` — the brief, the layout, the writing,
6
+ * read off the files. The platform's half — the judge's word, the
7
+ * repository, the build, the preview, and the panel's setting up — comes
8
+ * through the dev session (`jtkit session`). This turns the two into the
9
+ * strip the page shows, by the same rules the panel's own card uses
10
+ * (entities/site/writing.ts there): a phase is a fact; a later phase
11
+ * counts only once every phase before it does; written means the kit's
12
+ * still-to-do list is empty, not that the required fields are filled.
13
+ */
14
+
15
+ export const PHASES = ['brief', 'laidOut', 'writing', 'judged', 'attached', 'building', 'preview'];
16
+
17
+ export const PHASE_NAMES = {
18
+ brief: 'Brief',
19
+ laidOut: 'Laid out',
20
+ writing: 'Writing',
21
+ judged: 'Checked',
22
+ attached: 'Attached',
23
+ building: 'Building',
24
+ preview: 'Preview',
25
+ };
26
+
27
+ /** The panel's steps, as the page names them; `agent` ones are not the developer's. */
28
+ export const STEP_NAMES = {
29
+ build: 'The agent builds it',
30
+ diary: 'Bookings: the rules and what is booked',
31
+ business: 'Who owns this site',
32
+ notifications: 'Where enquiries and bookings go',
33
+ subscription: 'The subscription',
34
+ access: 'What the studio may do',
35
+ payee: 'Who gets paid',
36
+ launch: 'First publish',
37
+ };
38
+
39
+ /**
40
+ * Where the site is.
41
+ *
42
+ * @param {import('./progress.mjs').Progress | null | undefined} progress the kit's reading, or null where none
43
+ * @param {object | null | undefined} platform the session's answer (`/v1/dev-sessions/{id}/progress`), or null where not connected
44
+ */
45
+ export function phasesOf(progress, platform) {
46
+ const seen = progress !== null && progress !== undefined;
47
+ const known = platform !== null && platform !== undefined && platform.error === undefined;
48
+ const attached = known && platform.attached === true;
49
+ const served = known && platform.served === true;
50
+ const build = known && platform.build ? platform.build : null;
51
+
52
+ const briefDone = seen
53
+ ? progress.brief !== null && progress.brief.facts > 0 && progress.brief.sections['Decisions'] === true
54
+ : attached;
55
+ const laidOutDone = seen ? progress.kit !== null || progress.catalogue === true : attached;
56
+ const writingDone = seen
57
+ ? progress.pages.length > 0 &&
58
+ progress.pages.every((one) => one.written) &&
59
+ Object.values(progress.collections).every((one) => one.entries >= one.expected) &&
60
+ progress.missing.length === 0
61
+ : attached;
62
+ const judgedDone = known && platform.judged?.ok === true;
63
+ const buildingDone = served || build?.status === 'ok';
64
+ const buildingCurrent = build !== null && (build.status === 'running' || build.status === 'queued' || build.status === 'failed');
65
+
66
+ const done = {
67
+ brief: briefDone,
68
+ laidOut: laidOutDone,
69
+ writing: writingDone,
70
+ judged: judgedDone,
71
+ attached,
72
+ building: buildingDone,
73
+ preview: served,
74
+ };
75
+ // In order: a later phase counts only once every phase before it does.
76
+ let before = true;
77
+ for (const key of PHASES) {
78
+ done[key] = done[key] && before;
79
+ before = done[key];
80
+ }
81
+
82
+ const current = buildingCurrent && !served ? 'building' : (PHASES.find((key) => !done[key]) ?? 'preview');
83
+ const index = PHASES.indexOf(current);
84
+ const phases = PHASES.map((key, i) => {
85
+ let state;
86
+ if (key === current) state = 'current';
87
+ else if (done[key]) state = 'done';
88
+ else if (!seen && !attached && i < 3) state = 'unseen';
89
+ else if (!known && i >= 3) state = 'unseen';
90
+ else if (i < index) state = 'unseen';
91
+ else state = 'left';
92
+ return { key, state };
93
+ });
94
+ return { phases, current, index, known, seen };
95
+ }
96
+
97
+ /** The sentence under the strip, for the current phase. */
98
+ export function sayPhase(where, progress, platform) {
99
+ const known = platform !== null && platform !== undefined && platform.error === undefined;
100
+ switch (where.current) {
101
+ case 'brief':
102
+ return 'Collecting the brief';
103
+ case 'laidOut':
104
+ return 'Laying the site out';
105
+ case 'writing': {
106
+ const pages = progress?.pages ?? [];
107
+ const written = pages.filter((one) => one.written).length;
108
+ return `Writing the pages — ${written} of ${pages.length} with their text · still to do: ${progress?.missing.length ?? 0}`;
109
+ }
110
+ case 'judged':
111
+ if (!known || !platform.judged) return 'To be checked before the push';
112
+ return platform.judged.ok ? 'Checked, clean' : `Checked — findings to fix: ${platform.judged.findings}`;
113
+ case 'attached':
114
+ return 'To be pushed and attached';
115
+ case 'building': {
116
+ const build = known ? platform.build : null;
117
+ if (!build) return 'To be built';
118
+ if (build.status === 'failed') return `The build failed — ${build.error || 'see its log in the panel'}`;
119
+ if (build.status === 'queued') return 'A build is queued';
120
+ return build.stage_of ? `Building — step ${build.stage_num} of ${build.stage_of}` : 'Building';
121
+ }
122
+ default:
123
+ return 'The preview is ready';
124
+ }
125
+ }
126
+
127
+ /**
128
+ * What the developer can do in the panel meanwhile: the setting up's steps
129
+ * that are not the agent's — open first, then done.
130
+ */
131
+ export function meanwhile(platform) {
132
+ const steps = platform?.steps ?? [];
133
+ const named = (step) => ({ key: step.key, name: STEP_NAMES[step.key] ?? step.key, done: step.done === true });
134
+ const theirs = steps.filter((step) => step.whose !== 'agent');
135
+ return {
136
+ open: theirs.filter((step) => !step.done).map(named),
137
+ done: theirs.filter((step) => step.done).map(named),
138
+ };
139
+ }