@jtakeit/kit 0.1.0 → 0.3.0
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 +1 -1
- package/bin/jtkit.mjs +17 -0
- package/elements/booking.js +48 -7
- package/lib/booking/render.mjs +39 -5
- package/lib/booking/words.mjs +262 -4
- package/lib/build-codes.mjs +29 -0
- package/lib/catalogue.mjs +50 -10
- package/lib/content.mjs +101 -0
- package/lib/kits.mjs +1 -1
- package/lib/lint.mjs +369 -0
- package/package.json +51 -13
- package/types/booking.d.ts +4 -0
- package/types/content.d.ts +9 -0
- package/types/lint.d.ts +19 -0
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ shape, so the form says what it sends and the element sends nothing else.
|
|
|
31
31
|
import { renderBookingForm } from '@jtakeit/kit/booking';
|
|
32
32
|
|
|
33
33
|
const html = renderBookingForm({
|
|
34
|
-
services, // [{ slug, title, takes, costs, group }], the entries with `takes`
|
|
34
|
+
services, // [{ slug, title, takes, costs, rates, group }], the entries with `takes`; `rates` instead of `costs` says «from …»
|
|
35
35
|
resources, // [{ slug, title }] — leave empty for a solo business
|
|
36
36
|
locale: 'uk', // uk, de or en: the words before the platform has spoken
|
|
37
37
|
combine: settings.combine === true, // jtk/bookings.json
|
package/bin/jtkit.mjs
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* kit's types, collections and module into jtk/catalogue.json
|
|
8
8
|
* jtkit catalogue [--declaration src/content/blocks.ts] [--emit-only] [--dist ./dist] [--judge]
|
|
9
9
|
* write jtk/catalogue.json from a declaration and check it against the built pages
|
|
10
|
+
* jtkit lint [--dist dist] [--root .] [--site-url https://…]
|
|
11
|
+
* the annotation lint the platform's build runs, on this working tree
|
|
10
12
|
*/
|
|
11
13
|
import { pathToFileURL } from 'node:url';
|
|
12
14
|
import { join, dirname, resolve } from 'node:path';
|
|
@@ -44,6 +46,21 @@ switch (command) {
|
|
|
44
46
|
}
|
|
45
47
|
break;
|
|
46
48
|
}
|
|
49
|
+
case 'lint': {
|
|
50
|
+
// The annotation lint the platform's build runs, on this working tree:
|
|
51
|
+
// `dist/` against `jtk/` and the catalogue, every finding at once,
|
|
52
|
+
// before any push (core wiki/71 · §3.2).
|
|
53
|
+
const { lintCommand } = await import(pathToFileURL(join(HERE, '..', 'lib', 'lint.mjs')).href);
|
|
54
|
+
const root = resolve(flag('root', process.cwd()));
|
|
55
|
+
const dist = resolve(root, flag('dist', 'dist'));
|
|
56
|
+
try {
|
|
57
|
+
process.exit(await lintCommand({ root, dist, siteURL: flag('site-url', process.env.SITE_URL ?? '') }));
|
|
58
|
+
} catch (why) {
|
|
59
|
+
console.error(`jtkit lint: ${why.message}`);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
47
64
|
case 'catalogue':
|
|
48
65
|
case 'catalog': {
|
|
49
66
|
process.argv = [process.argv[0], process.argv[1], ...rest];
|
package/elements/booking.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* GET /api/turnstile the widget's key, or "" for none
|
|
11
11
|
* GET /api/availability?service=…&from=…&to=…[&resource=…][&party=…][&count=…]
|
|
12
|
-
* → { zone, slots[{start, free, ends}], party_max, scale, services[{slug,
|
|
12
|
+
* → { zone, slots[{start, free, ends, costs_minor}], party_max, quantity_max, scale, services[{slug,
|
|
13
13
|
* scale, takes, fewest, most}], prices{class: minor}, currency, … }
|
|
14
14
|
* POST /api/book → { id, state, amount_minor, currency, token, manage_path, telegram_url }
|
|
15
15
|
* POST /api/pay → { url } — only when amount_minor > 0
|
|
@@ -113,13 +113,18 @@ export function wireBooking(form) {
|
|
|
113
113
|
const chosenBoxes = boxes.filter((b) => b.checked);
|
|
114
114
|
if (chosenBoxes.length < 2) { total.hidden = true; return; }
|
|
115
115
|
const minutes = chosenBoxes.reduce((sum, b) => sum + Number(b.dataset.takes ?? 0), 0);
|
|
116
|
-
|
|
116
|
+
// A price per person is times the party (wiki/70); the slot's price is
|
|
117
|
+
// the platform's, this is the list's own running sum.
|
|
118
|
+
const people = partyField.hidden ? 1 : Number(party.value) || 1;
|
|
119
|
+
const minor = chosenBoxes.reduce((sum, b) => sum + Number(b.dataset.costs ?? 0) * (b.dataset.per === 'person' ? people : 1), 0);
|
|
117
120
|
const price = minor > 0 ? ' · ' + money(minor, form.dataset.currency, pageLang()) : '';
|
|
118
121
|
total.textContent = `${words.total}: ${minutes} min${price}`;
|
|
119
122
|
total.hidden = false;
|
|
120
123
|
}
|
|
121
124
|
const resource = /** @type {HTMLSelectElement | null} */ (form.querySelector('[name=resource]'));
|
|
122
125
|
/** @type {HTMLSelectElement} */ const party = q('[name=party]');
|
|
126
|
+
/** @type {HTMLSelectElement} */ const quantity = q('[name=quantity]');
|
|
127
|
+
/** @type {HTMLElement} */ const quantityField = q('[data-quantity-field]');
|
|
123
128
|
/** @type {HTMLSelectElement} */ const hours = q('[name=hours]');
|
|
124
129
|
/** @type {HTMLElement} */ const hoursField = q('[data-hours-field]');
|
|
125
130
|
/** @type {HTMLElement} */ const partyField = q('[data-party-field]');
|
|
@@ -221,10 +226,34 @@ export function wireBooking(form) {
|
|
|
221
226
|
}
|
|
222
227
|
if (partyField.hidden) {
|
|
223
228
|
partyField.hidden = false;
|
|
224
|
-
party.addEventListener('change', () => void load());
|
|
229
|
+
party.addEventListener('change', () => { showTotal(); void load(); });
|
|
225
230
|
}
|
|
226
231
|
}
|
|
227
232
|
|
|
233
|
+
/**
|
|
234
|
+
* How many of the class one request may take (wiki/69): the settings'
|
|
235
|
+
* most, and no more than are free at the chosen time. Hidden at one, and
|
|
236
|
+
* when a resource is named — a name is one thing.
|
|
237
|
+
* @param {number} most
|
|
238
|
+
* @param {number} free
|
|
239
|
+
*/
|
|
240
|
+
function offerQuantity(most, free) {
|
|
241
|
+
if (most <= 1 || resourceChosen()) { quantityField.hidden = true; return; }
|
|
242
|
+
const cap = Math.max(1, Math.min(most, free || most));
|
|
243
|
+
const was = quantity.value;
|
|
244
|
+
quantity.textContent = '';
|
|
245
|
+
for (let n = 1; n <= cap; n++) {
|
|
246
|
+
const option = document.createElement('option');
|
|
247
|
+
option.value = String(n);
|
|
248
|
+
option.textContent = String(n);
|
|
249
|
+
quantity.appendChild(option);
|
|
250
|
+
}
|
|
251
|
+
quantity.value = was && Number(was) <= cap ? was : '1';
|
|
252
|
+
quantityField.hidden = false;
|
|
253
|
+
}
|
|
254
|
+
function quantityChosen() {
|
|
255
|
+
return quantityField.hidden ? 1 : Number(quantity.value) || 1;
|
|
256
|
+
}
|
|
228
257
|
/** @param {string} text */
|
|
229
258
|
function say(text) {
|
|
230
259
|
error.textContent = text;
|
|
@@ -471,6 +500,7 @@ export function wireBooking(form) {
|
|
|
471
500
|
zone = data.zone;
|
|
472
501
|
if (typeof data.scale === 'string') daily = data.scale === 'daily';
|
|
473
502
|
offerParty(data.party_max ?? 0);
|
|
503
|
+
offerQuantity(data.quantity_max ?? 1, Math.max(0, ...(data.slots ?? []).map((/** @type {{free?: string[]}} */ s) => (s.free ?? []).length)));
|
|
474
504
|
// A platform that says what each service may be taken as: the second
|
|
475
505
|
// date follows it. The first answer may change what was asked — the
|
|
476
506
|
// shortest stay filled in — and is asked once more with it.
|
|
@@ -507,8 +537,12 @@ export function wireBooking(form) {
|
|
|
507
537
|
button.textContent = daily
|
|
508
538
|
? slot.ends ? `${dayAndHour(slot.start, false)} — ${dayAndHour(slot.ends, false)}` : dayAndHour(slot.start, false)
|
|
509
539
|
: clock(slot.start);
|
|
540
|
+
// What the booking starting here costs — the platform's number, every
|
|
541
|
+
// hour by its own row (wiki/68) — beside the time, whenever one came.
|
|
542
|
+
if (slot.costs_minor > 0) button.textContent += ` · ${money(slot.costs_minor, currency, pageLang())}`;
|
|
510
543
|
button.addEventListener('click', () => {
|
|
511
544
|
chosen = slot.start;
|
|
545
|
+
offerQuantity(data.quantity_max ?? 1, (slot.free ?? []).length);
|
|
512
546
|
for (const other of slots.querySelectorAll('.booking__slot')) {
|
|
513
547
|
other.classList.remove('is-chosen');
|
|
514
548
|
other.setAttribute('aria-pressed', 'false');
|
|
@@ -565,6 +599,7 @@ export function wireBooking(form) {
|
|
|
565
599
|
// How many of the service — never an end: the platform computes
|
|
566
600
|
// the end and the price from it.
|
|
567
601
|
...(count() ? { count: count() } : {}),
|
|
602
|
+
...(quantityChosen() > 1 ? { quantity: quantityChosen() } : {}),
|
|
568
603
|
start: chosen,
|
|
569
604
|
name, phone, email,
|
|
570
605
|
note: String(data.get('note') ?? ''),
|
|
@@ -599,7 +634,7 @@ export function wireBooking(form) {
|
|
|
599
634
|
});
|
|
600
635
|
|
|
601
636
|
/**
|
|
602
|
-
* @param {{ state: string; start: string; ends: string; amount_minor: number; currency: string; id: string; manage_path: string; telegram_url: string; resource?: string; class?: string }} made
|
|
637
|
+
* @param {{ state: string; start: string; ends: string; amount_minor: number; currency: string; id: string; manage_path: string; telegram_url: string; resource?: string; class?: string; group?: { quantity: number; amount_minor: number; bookings: { resource: string; resource_name?: string }[] } }} made
|
|
603
638
|
*/
|
|
604
639
|
function done(made) {
|
|
605
640
|
for (const el of /** @type {NodeListOf<HTMLElement>} */ (form.querySelectorAll('.field, .booking__times, .booking__services, [data-submit], [data-challenge]'))) el.hidden = true;
|
|
@@ -615,7 +650,11 @@ export function wireBooking(form) {
|
|
|
615
650
|
// What was taken: the class promised, where «any free» was chosen; the
|
|
616
651
|
// one by name and marks, where it was chosen — «VW Combi · білий ·
|
|
617
652
|
// AA1234BB». Nothing where the business has one of everything.
|
|
618
|
-
|
|
653
|
+
let what = made.class ? words.anyOf.replace('{class}', made.class) : resourceChosen() ? made.resource ?? '' : '';
|
|
654
|
+
// A group: the resources it took, and how many together (wiki/69).
|
|
655
|
+
if (made.group && made.group.quantity > 1) {
|
|
656
|
+
what = `${made.group.bookings.map((/** @type {{resource_name?: string, resource: string}} */ b) => b.resource_name || b.resource).join(', ')} · ${words.together.replace('{n}', String(made.group.quantity))}`;
|
|
657
|
+
}
|
|
619
658
|
if (what) {
|
|
620
659
|
/** @type {HTMLElement} */ const line = q('[data-done-what]');
|
|
621
660
|
line.textContent = words.took.replace('{what}', what);
|
|
@@ -627,12 +666,14 @@ export function wireBooking(form) {
|
|
|
627
666
|
/** @type {HTMLAnchorElement} */ const tg = q('[data-telegram]');
|
|
628
667
|
if (made.telegram_url) { tg.href = made.telegram_url; tg.textContent = words.telegram; tg.hidden = false; }
|
|
629
668
|
|
|
630
|
-
|
|
669
|
+
// The group is paid for together: its sum on the button.
|
|
670
|
+
const owed = made.group && made.group.amount_minor > 0 ? made.group.amount_minor : made.amount_minor;
|
|
671
|
+
if (owed > 0) {
|
|
631
672
|
/** @type {HTMLElement} */ const note = q('[data-pay-note]');
|
|
632
673
|
note.textContent = words.payNote;
|
|
633
674
|
note.hidden = false;
|
|
634
675
|
/** @type {HTMLAnchorElement} */ const pay = q('[data-pay]');
|
|
635
|
-
pay.textContent = `${words.pay} · ${money(
|
|
676
|
+
pay.textContent = `${words.pay} · ${money(owed, made.currency, pageLang())}`;
|
|
636
677
|
pay.href = '#';
|
|
637
678
|
pay.hidden = false;
|
|
638
679
|
pay.addEventListener('click', async (event) => {
|
package/lib/booking/render.mjs
CHANGED
|
@@ -38,7 +38,7 @@ import { wordsFor } from './words.mjs';
|
|
|
38
38
|
* sends no key that is not here — the list cannot drift from the request,
|
|
39
39
|
* because the element imports this one.
|
|
40
40
|
*/
|
|
41
|
-
export const SENDS = Object.freeze(['service', 'services', 'resource', 'class', 'party', 'count', 'start', 'name', 'phone', 'email', 'note', 'turnstile']);
|
|
41
|
+
export const SENDS = Object.freeze(['service', 'services', 'resource', 'class', 'party', 'count', 'quantity', 'start', 'name', 'phone', 'email', 'note', 'turnstile']);
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
44
|
* @typedef {object} Named
|
|
@@ -46,9 +46,20 @@ export const SENDS = Object.freeze(['service', 'services', 'resource', 'class',
|
|
|
46
46
|
* @property {string} title what the visitor reads
|
|
47
47
|
* @property {number} [takes] minutes, for the running total when several are picked
|
|
48
48
|
* @property {number} [costs] minor units, for the same
|
|
49
|
+
* @property {{ day: string, from: number, until: number, costs: number }[]} [rates] a price by the hour of the week instead of `costs`: the list says «from …», and the price is the slot's, printed beside the time
|
|
50
|
+
* @property {string} [per] `'person'` for a price times the party — a class per place, shoes; the list says so and the running total multiplies
|
|
49
51
|
* @property {string} [group] a heading the services are listed under
|
|
50
52
|
*/
|
|
51
53
|
|
|
54
|
+
/**
|
|
55
|
+
* The least a service priced by the hour of the week may cost, or nothing.
|
|
56
|
+
* @param {Named} one
|
|
57
|
+
*/
|
|
58
|
+
export function priceFrom(one) {
|
|
59
|
+
const rows = Array.isArray(one.rates) ? one.rates : [];
|
|
60
|
+
return rows.reduce((least, row) => (row.costs > 0 && (least === 0 || row.costs < least) ? row.costs : least), 0);
|
|
61
|
+
}
|
|
62
|
+
|
|
52
63
|
/**
|
|
53
64
|
* @typedef {object} BookingFormProps
|
|
54
65
|
* @property {Named[]} services
|
|
@@ -70,6 +81,20 @@ function esc(value) {
|
|
|
70
81
|
return String(value ?? '').replace(/[&<>"']/g, (c) => ESCAPE[c]);
|
|
71
82
|
}
|
|
72
83
|
|
|
84
|
+
/**
|
|
85
|
+
* A service's price as the list says it: the price, or «from …» for one
|
|
86
|
+
* priced by the hour of the week — whose price is the slot's and is printed
|
|
87
|
+
* beside the time once one is chosen (wiki/68).
|
|
88
|
+
* @param {Named} one
|
|
89
|
+
* @param {string} currency
|
|
90
|
+
* @param {string} lang
|
|
91
|
+
*/
|
|
92
|
+
function priceSaid(one, currency, lang) {
|
|
93
|
+
if (one.costs) return ` · ${esc(money(one.costs, currency, lang))}`;
|
|
94
|
+
const least = priceFrom(one);
|
|
95
|
+
return least > 0 ? ` · ${esc(wordsFor(lang).from)} ${esc(money(least, currency, lang))}` : '';
|
|
96
|
+
}
|
|
97
|
+
|
|
73
98
|
/**
|
|
74
99
|
* The booking form as HTML.
|
|
75
100
|
*
|
|
@@ -84,7 +109,7 @@ export function renderBookingForm(props) {
|
|
|
84
109
|
if (!Array.isArray(services)) throw new TypeError('renderBookingForm: services is a list of { slug, title }');
|
|
85
110
|
const unsaid = kind !== '' && kind !== 'salon';
|
|
86
111
|
const WORDS = wordsFor(locale, scale);
|
|
87
|
-
const lang =
|
|
112
|
+
const lang = { de: 'de-CH', uk: 'uk-UA', ru: 'ru-RU', es: 'es-ES', it: 'it-IT', pt: 'pt-PT', fr: 'fr-FR' }[locale] ?? 'en';
|
|
88
113
|
|
|
89
114
|
// The checklist's runs, in the order the services came, each heading once.
|
|
90
115
|
/** @type {{ group: string; items: Named[] }[]} */
|
|
@@ -99,15 +124,19 @@ export function renderBookingForm(props) {
|
|
|
99
124
|
const today = now.toISOString().slice(0, 10);
|
|
100
125
|
const last = new Date(now.getTime() + daysAhead * 86_400_000).toISOString().slice(0, 10);
|
|
101
126
|
|
|
127
|
+
// An add-on — a price and no length: shoes, a racket — is listed under
|
|
128
|
+
// its own heading, after the services, and rides on the visit (wiki/70).
|
|
129
|
+
const addOns = runs.flatMap((run) => run.items.filter((one) => !one.takes && (one.costs || priceFrom(one))));
|
|
130
|
+
const timedRuns = runs.map((run) => ({ ...run, items: run.items.filter((one) => !addOns.includes(one)) })).filter((run) => run.items.length > 0);
|
|
102
131
|
const servicesField = combine
|
|
103
132
|
? `<fieldset class="booking__services" data-services>
|
|
104
133
|
<legend class="field__label" data-say="services">${esc(WORDS.services)}</legend>
|
|
105
|
-
${
|
|
134
|
+
${[...timedRuns, ...(addOns.length ? [{ group: WORDS.addOns, items: addOns }] : [])].map((run) => `<div class="booking__run">
|
|
106
135
|
${run.group !== '' ? `<p class="booking__run-title">${esc(run.group)}</p>` : ''}
|
|
107
136
|
${run.items.map((one) => `<label class="booking__service">
|
|
108
|
-
<input type="checkbox" name="service" value="${esc(one.slug)}" data-takes="${esc(one.takes ?? 0)}" data-costs="${esc(one.costs ?? 0)}">
|
|
137
|
+
<input type="checkbox" name="service" value="${esc(one.slug)}" data-takes="${esc(one.takes ?? 0)}" data-costs="${esc(one.costs ?? 0)}"${one.per === 'person' ? ' data-per="person"' : ''}>
|
|
109
138
|
<span class="booking__service-name">${esc(one.title)}</span>
|
|
110
|
-
<span class="booking__service-meta">${one.takes ? `${esc(one.takes)} min` : ''}${one.
|
|
139
|
+
<span class="booking__service-meta">${one.takes ? `${esc(one.takes)} min` : ''}${priceSaid(one, currency, lang)}${one.per === 'person' ? ` · ${esc(WORDS.perPerson)}` : ''}</span>
|
|
111
140
|
</label>`).join('\n')}
|
|
112
141
|
</div>`).join('\n')}
|
|
113
142
|
<p class="booking__total" data-total hidden></p>
|
|
@@ -163,6 +192,11 @@ export function renderBookingForm(props) {
|
|
|
163
192
|
<select class="field__input" id="booking-party" name="party"></select>
|
|
164
193
|
</div>
|
|
165
194
|
|
|
195
|
+
<div class="field" data-quantity-field hidden>
|
|
196
|
+
<label class="field__label" for="booking-quantity">${esc(WORDS.quantity)}</label>
|
|
197
|
+
<select class="field__input" id="booking-quantity" name="quantity"></select>
|
|
198
|
+
</div>
|
|
199
|
+
|
|
166
200
|
<fieldset class="field booking__times" data-times>
|
|
167
201
|
<legend class="field__label">${esc(WORDS.time)}</legend>
|
|
168
202
|
<p class="booking__hint" data-times-hint aria-live="polite">${esc(WORDS.pick)}</p>
|
package/lib/booking/words.mjs
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
/** The languages the form carries words in. */
|
|
13
|
-
export const LOCALES = Object.freeze(['uk', 'de', 'en']);
|
|
13
|
+
export const LOCALES = Object.freeze(['uk', 'de', 'en', 'ru', 'es', 'it', 'pt', 'fr']);
|
|
14
14
|
|
|
15
15
|
const BASE = {
|
|
16
16
|
uk: {
|
|
@@ -22,7 +22,8 @@ const BASE = {
|
|
|
22
22
|
manage: 'Скасувати або перенести запис', telegram: 'Нагадування в Telegram',
|
|
23
23
|
taken: 'Цей час щойно зайняли — оберіть інший.', failed: 'Не вдалося записатися. Спробуйте ще раз або зателефонуйте.',
|
|
24
24
|
back: 'До', length: 'Від {min} до {max} діб.', anyOf: 'Будь-яке вільне — {class}', noneOf: '{class} — немає вільних', notFor: '{who} — не для обраного', notForChosen: '{who} — не для обраного. Оберіть іншого або «{any}».', took: 'Ви берете: {what}',
|
|
25
|
-
party: 'Скільки вас', hours: 'Скільки часу', hourShort: 'год', minuteShort: 'хв', challenge: 'Підтвердіть, що ви не робот, і спробуйте ще раз.', tooMany: 'У вас уже є кілька відкритих записів. Скасуйте один із них або зателефонуйте нам.',
|
|
25
|
+
party: 'Скільки вас', perPerson: 'за особу', addOns: 'Додатково', quantity: 'Скільки — доріжок, столів, кімнат', together: '{n} разом', hours: 'Скільки часу', hourShort: 'год', minuteShort: 'хв', challenge: 'Підтвердіть, що ви не робот, і спробуйте ще раз.', tooMany: 'У вас уже є кілька відкритих записів. Скасуйте один із них або зателефонуйте нам.',
|
|
26
|
+
from: 'від',
|
|
26
27
|
needContact: 'Вкажіть телефон або пошту, щоб ми могли з вами звʼязатися.', noScript: 'Онлайн-форма працює з увімкненим JavaScript — увімкніть його або зателефонуйте нам.',
|
|
27
28
|
},
|
|
28
29
|
de: {
|
|
@@ -34,7 +35,8 @@ const BASE = {
|
|
|
34
35
|
manage: 'Termin absagen oder verschieben', telegram: 'Erinnerungen per Telegram',
|
|
35
36
|
taken: 'Diese Zeit wurde gerade vergeben — bitte wählen Sie eine andere.', failed: 'Die Buchung hat nicht geklappt. Bitte erneut versuchen oder anrufen.',
|
|
36
37
|
back: 'Bis', length: 'Von {min} bis {max} Nächte.', anyOf: 'Beliebig frei — {class}', noneOf: '{class} — nichts verfügbar', notFor: '{who} — nicht für diese Auswahl', notForChosen: '{who} — nicht für diese Auswahl. Bitte jemand anderen oder «{any}» wählen.', took: 'Sie nehmen: {what}',
|
|
37
|
-
party: 'Wie viele Personen', hours: 'Wie lange', hourShort: 'Std.', minuteShort: 'Min.', challenge: 'Bitte bestätigen Sie, dass Sie kein Roboter sind, und versuchen Sie es erneut.', tooMany: 'Sie haben bereits mehrere offene Termine. Sagen Sie einen ab oder rufen Sie uns an.',
|
|
38
|
+
party: 'Wie viele Personen', perPerson: 'pro Person', addOns: 'Zusätzlich', quantity: 'Wie viele — Bahnen, Tische, Zimmer', together: '{n} zusammen', hours: 'Wie lange', hourShort: 'Std.', minuteShort: 'Min.', challenge: 'Bitte bestätigen Sie, dass Sie kein Roboter sind, und versuchen Sie es erneut.', tooMany: 'Sie haben bereits mehrere offene Termine. Sagen Sie einen ab oder rufen Sie uns an.',
|
|
39
|
+
from: 'ab',
|
|
38
40
|
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.',
|
|
39
41
|
},
|
|
40
42
|
en: {
|
|
@@ -46,9 +48,225 @@ const BASE = {
|
|
|
46
48
|
manage: 'Cancel or move the booking', telegram: 'Reminders in Telegram',
|
|
47
49
|
taken: 'That time has just been taken — please choose another.', failed: 'That did not work. Please try again or ring us.',
|
|
48
50
|
back: 'Until', length: 'From {min} to {max} nights.', anyOf: 'Any free — {class}', noneOf: '{class} — none available', notFor: '{who} — not for this choice', notForChosen: '{who} — not for this choice. Pick another, or «{any}».', took: 'You are taking: {what}',
|
|
49
|
-
party: 'How many of you', hours: 'How long', hourShort: 'h', minuteShort: 'min', challenge: 'Please confirm you are not a robot and try again.', tooMany: 'You already have several open bookings. Cancel one, or ring us.',
|
|
51
|
+
party: 'How many of you', perPerson: 'per person', addOns: 'Add-ons', quantity: 'How many — lanes, tables, rooms', together: '{n} together', hours: 'How long', hourShort: 'h', minuteShort: 'min', challenge: 'Please confirm you are not a robot and try again.', tooMany: 'You already have several open bookings. Cancel one, or ring us.',
|
|
52
|
+
from: 'from',
|
|
50
53
|
needContact: 'Leave a phone number or an email so we can reach you.', noScript: 'Booking online needs JavaScript — or ring us.',
|
|
51
54
|
},
|
|
55
|
+
ru: {
|
|
56
|
+
service: "Услуга",
|
|
57
|
+
services: "Услуги",
|
|
58
|
+
total: "Итого",
|
|
59
|
+
pickService: "Выберите хотя бы одну услугу.",
|
|
60
|
+
resource: "Мастер",
|
|
61
|
+
any: "Любой",
|
|
62
|
+
day: "День",
|
|
63
|
+
time: "Время",
|
|
64
|
+
name: "Имя",
|
|
65
|
+
phone: "Телефон",
|
|
66
|
+
email: "Электронная почта",
|
|
67
|
+
note: "Комментарий",
|
|
68
|
+
book: "Записаться",
|
|
69
|
+
sending: "Записываем…",
|
|
70
|
+
none: "В этот день свободного времени нет.",
|
|
71
|
+
noParty: "Для такой компании свободных мест нет.",
|
|
72
|
+
pick: "Выберите день",
|
|
73
|
+
confirmed: "Вы записаны.",
|
|
74
|
+
pending: "Запись получена — мы подтвердим ее в ближайшее время.",
|
|
75
|
+
pay: "Оплатить",
|
|
76
|
+
payNote: "Время зарезервировано на 30 минут. Чтобы закрепить его, завершите оплату.",
|
|
77
|
+
manage: "Отменить или перенести запись",
|
|
78
|
+
telegram: "Напоминания в Telegram",
|
|
79
|
+
taken: "Это время только что заняли — выберите другое.",
|
|
80
|
+
failed: "Не удалось записаться. Попробуйте еще раз или позвоните.",
|
|
81
|
+
back: "До",
|
|
82
|
+
length: "От {min} до {max} суток.",
|
|
83
|
+
anyOf: "Любой свободный — {class}",
|
|
84
|
+
noneOf: "{class} — свободных нет",
|
|
85
|
+
notFor: "{who} — не для выбранного",
|
|
86
|
+
notForChosen: "{who} — не для выбранного. Выберите другого или «{any}».",
|
|
87
|
+
took: "Вы берете: {what}",
|
|
88
|
+
party: "Сколько вас", perPerson: 'за человека', addOns: 'Дополнительно', quantity: 'Сколько — дорожек, столов, комнат', together: '{n} вместе',
|
|
89
|
+
hours: "На сколько времени",
|
|
90
|
+
hourShort: "ч",
|
|
91
|
+
minuteShort: "мин",
|
|
92
|
+
challenge: "Подтвердите, что вы не робот, и попробуйте еще раз.",
|
|
93
|
+
tooMany: "У вас уже есть несколько открытых записей. Отмените одну из них или позвоните нам.",
|
|
94
|
+
from: 'от',
|
|
95
|
+
needContact: "Укажите телефон или почту, чтобы мы могли с вами связаться.",
|
|
96
|
+
noScript: "Онлайн-форма работает с включенным JavaScript — включите его или позвоните нам.",
|
|
97
|
+
},
|
|
98
|
+
es: {
|
|
99
|
+
service: "Servicio",
|
|
100
|
+
services: "Servicios",
|
|
101
|
+
total: "Total",
|
|
102
|
+
pickService: "Elija al menos un servicio.",
|
|
103
|
+
resource: "Con",
|
|
104
|
+
any: "Cualquiera",
|
|
105
|
+
day: "Día",
|
|
106
|
+
time: "Hora",
|
|
107
|
+
name: "Nombre",
|
|
108
|
+
phone: "Teléfono",
|
|
109
|
+
email: "Correo electrónico",
|
|
110
|
+
note: "Comentario",
|
|
111
|
+
book: "Reservar",
|
|
112
|
+
sending: "Reservando…",
|
|
113
|
+
none: "Ese día no queda nada libre.",
|
|
114
|
+
noParty: "No hay sitio para un grupo tan grande.",
|
|
115
|
+
pick: "Elija un día",
|
|
116
|
+
confirmed: "Su reserva está hecha.",
|
|
117
|
+
pending: "Recibida: la confirmaremos en breve.",
|
|
118
|
+
pay: "Pagar ahora",
|
|
119
|
+
payNote: "La hora queda retenida 30 minutos. Complete el pago para conservarla.",
|
|
120
|
+
manage: "Cancelar o mover la reserva",
|
|
121
|
+
telegram: "Recordatorios en Telegram",
|
|
122
|
+
taken: "Esa hora acaba de ocuparse; elija otra, por favor.",
|
|
123
|
+
failed: "No ha funcionado. Inténtelo de nuevo o llámenos.",
|
|
124
|
+
back: "Hasta",
|
|
125
|
+
length: "De {min} a {max} noches.",
|
|
126
|
+
anyOf: "Cualquiera libre — {class}",
|
|
127
|
+
noneOf: "{class} — ninguno disponible",
|
|
128
|
+
notFor: "{who} — no para esta elección",
|
|
129
|
+
notForChosen: "{who} — no para esta elección. Elija otro, o «{any}».",
|
|
130
|
+
took: "Se lleva: {what}",
|
|
131
|
+
party: "Cuántos son", perPerson: 'por persona', addOns: 'Extras', quantity: 'Cuántas — pistas, mesas, habitaciones', together: '{n} juntas',
|
|
132
|
+
hours: "Cuánto tiempo",
|
|
133
|
+
hourShort: "h",
|
|
134
|
+
minuteShort: "min",
|
|
135
|
+
challenge: "Confirme que no es un robot e inténtelo de nuevo.",
|
|
136
|
+
tooMany: "Ya tiene varias reservas abiertas. Cancele una o llámenos.",
|
|
137
|
+
from: 'desde',
|
|
138
|
+
needContact: "Deje un teléfono o un correo electrónico para que podamos contactar con usted.",
|
|
139
|
+
noScript: "Para reservar en línea hace falta JavaScript — o llámenos.",
|
|
140
|
+
},
|
|
141
|
+
it: {
|
|
142
|
+
service: "Servizio",
|
|
143
|
+
services: "Servizi",
|
|
144
|
+
total: "Totale",
|
|
145
|
+
pickService: "Scelga almeno un servizio.",
|
|
146
|
+
resource: "Con",
|
|
147
|
+
any: "Chiunque",
|
|
148
|
+
day: "Giorno",
|
|
149
|
+
time: "Orario",
|
|
150
|
+
name: "Nome",
|
|
151
|
+
phone: "Telefono",
|
|
152
|
+
email: "Email",
|
|
153
|
+
note: "Nota",
|
|
154
|
+
book: "Prenota",
|
|
155
|
+
sending: "Prenotazione…",
|
|
156
|
+
none: "Niente di libero in quel giorno.",
|
|
157
|
+
noParty: "Non c'è posto per un gruppo così numeroso.",
|
|
158
|
+
pick: "Scelga un giorno",
|
|
159
|
+
confirmed: "Prenotazione effettuata.",
|
|
160
|
+
pending: "Ricevuta: confermeremo a breve.",
|
|
161
|
+
pay: "Paga ora",
|
|
162
|
+
payNote: "L'orario è tenuto per 30 minuti. Completi il pagamento per mantenerlo.",
|
|
163
|
+
manage: "Annulla o sposta la prenotazione",
|
|
164
|
+
telegram: "Promemoria su Telegram",
|
|
165
|
+
taken: "Questo orario è appena stato preso: ne scelga un altro.",
|
|
166
|
+
failed: "Non ha funzionato. Riprovi o ci chiami.",
|
|
167
|
+
back: "Fino a",
|
|
168
|
+
length: "Da {min} a {max} notti.",
|
|
169
|
+
anyOf: "Uno qualsiasi libero — {class}",
|
|
170
|
+
noneOf: "{class} — nessuno disponibile",
|
|
171
|
+
notFor: "{who} — non per questa scelta",
|
|
172
|
+
notForChosen: "{who} — non per questa scelta. Ne scelga un altro, o «{any}».",
|
|
173
|
+
took: "Sta prendendo: {what}",
|
|
174
|
+
party: "Quante persone", perPerson: 'a persona', addOns: 'Extra', quantity: 'Quante — piste, tavoli, camere', together: '{n} insieme',
|
|
175
|
+
hours: "Per quanto tempo",
|
|
176
|
+
hourShort: "h",
|
|
177
|
+
minuteShort: "min",
|
|
178
|
+
challenge: "Confermi di non essere un robot e riprovi.",
|
|
179
|
+
tooMany: "Ha già diverse prenotazioni aperte. Ne annulli una, o ci chiami.",
|
|
180
|
+
from: 'da',
|
|
181
|
+
needContact: "Lasci un numero di telefono o un'email per poterLa contattare.",
|
|
182
|
+
noScript: "Per prenotare online serve JavaScript: oppure ci chiami.",
|
|
183
|
+
},
|
|
184
|
+
pt: {
|
|
185
|
+
service: "Serviço",
|
|
186
|
+
services: "Serviços",
|
|
187
|
+
total: "Total",
|
|
188
|
+
pickService: "Escolha pelo menos um serviço.",
|
|
189
|
+
resource: "Com",
|
|
190
|
+
any: "Qualquer um",
|
|
191
|
+
day: "Dia",
|
|
192
|
+
time: "Hora",
|
|
193
|
+
name: "Nome",
|
|
194
|
+
phone: "Telefone",
|
|
195
|
+
email: "E-mail",
|
|
196
|
+
note: "Observações",
|
|
197
|
+
book: "Reservar",
|
|
198
|
+
sending: "A reservar…",
|
|
199
|
+
none: "Não há horários livres nesse dia.",
|
|
200
|
+
noParty: "Não há lugar para um grupo tão grande.",
|
|
201
|
+
pick: "Escolha um dia",
|
|
202
|
+
confirmed: "A sua reserva está feita.",
|
|
203
|
+
pending: "Pedido recebido — confirmamos em breve.",
|
|
204
|
+
pay: "Pagar agora",
|
|
205
|
+
payNote: "O horário fica reservado durante 30 minutos. Conclua o pagamento para o manter.",
|
|
206
|
+
manage: "Cancelar ou remarcar a reserva",
|
|
207
|
+
telegram: "Lembretes no Telegram",
|
|
208
|
+
taken: "Esse horário acabou de ser ocupado — escolha outro.",
|
|
209
|
+
failed: "Não foi possível reservar. Tente de novo ou ligue-nos.",
|
|
210
|
+
back: "Até",
|
|
211
|
+
length: "De {min} a {max} noites.",
|
|
212
|
+
anyOf: "Qualquer livre — {class}",
|
|
213
|
+
noneOf: "{class} — nenhum disponível",
|
|
214
|
+
notFor: "{who} — não para esta escolha",
|
|
215
|
+
notForChosen: "{who} — não para esta escolha. Escolha outro ou «{any}».",
|
|
216
|
+
took: "Leva: {what}",
|
|
217
|
+
party: "Quantos são", perPerson: 'por pessoa', addOns: 'Extras', quantity: 'Quantas — pistas, mesas, quartos', together: '{n} em conjunto',
|
|
218
|
+
hours: "Quanto tempo",
|
|
219
|
+
hourShort: "h",
|
|
220
|
+
minuteShort: "min",
|
|
221
|
+
challenge: "Confirme que não é um robô e tente de novo.",
|
|
222
|
+
tooMany: "Já tem várias reservas em aberto. Cancele uma ou ligue-nos.",
|
|
223
|
+
from: 'desde',
|
|
224
|
+
needContact: "Deixe um telefone ou um e-mail para podermos contactá-lo.",
|
|
225
|
+
noScript: "Reservar online precisa de JavaScript — ou ligue-nos.",
|
|
226
|
+
},
|
|
227
|
+
fr: {
|
|
228
|
+
service: "Prestation",
|
|
229
|
+
services: "Prestations",
|
|
230
|
+
total: "Total",
|
|
231
|
+
pickService: "Choisissez au moins une prestation.",
|
|
232
|
+
resource: "Avec",
|
|
233
|
+
any: "Sans préférence",
|
|
234
|
+
day: "Jour",
|
|
235
|
+
time: "Heure",
|
|
236
|
+
name: "Nom",
|
|
237
|
+
phone: "Téléphone",
|
|
238
|
+
email: "E-mail",
|
|
239
|
+
note: "Remarque",
|
|
240
|
+
book: "Réserver",
|
|
241
|
+
sending: "Réservation en cours…",
|
|
242
|
+
none: "Rien n’est libre ce jour-là.",
|
|
243
|
+
noParty: "Pas de place pour un groupe de cette taille.",
|
|
244
|
+
pick: "Choisissez un jour",
|
|
245
|
+
confirmed: "Votre réservation est confirmée.",
|
|
246
|
+
pending: "Demande reçue — nous confirmons sous peu.",
|
|
247
|
+
pay: "Payer maintenant",
|
|
248
|
+
payNote: "Le créneau est réservé pendant 30 minutes. Terminez le paiement pour le conserver.",
|
|
249
|
+
manage: "Annuler ou déplacer la réservation",
|
|
250
|
+
telegram: "Rappels dans Telegram",
|
|
251
|
+
taken: "Ce créneau vient d’être pris — veuillez en choisir un autre.",
|
|
252
|
+
failed: "Cela n’a pas fonctionné. Réessayez ou appelez-nous.",
|
|
253
|
+
back: "Jusqu’au",
|
|
254
|
+
length: "De {min} à {max} nuits.",
|
|
255
|
+
anyOf: "N’importe lequel de libre — {class}",
|
|
256
|
+
noneOf: "{class} — aucun disponible",
|
|
257
|
+
notFor: "{who} — pas pour ce choix",
|
|
258
|
+
notForChosen: "{who} — pas pour ce choix. Choisissez-en un autre, ou « {any} ».",
|
|
259
|
+
took: "Vous prenez : {what}",
|
|
260
|
+
party: "Combien êtes-vous", perPerson: 'par personne', addOns: 'Options', quantity: 'Combien — pistes, tables, chambres', together: '{n} ensemble',
|
|
261
|
+
hours: "Combien de temps",
|
|
262
|
+
hourShort: "h",
|
|
263
|
+
minuteShort: "min",
|
|
264
|
+
challenge: "Veuillez confirmer que vous n’êtes pas un robot et réessayer.",
|
|
265
|
+
tooMany: "Vous avez déjà plusieurs réservations en cours. Annulez-en une, ou appelez-nous.",
|
|
266
|
+
from: 'à partir de',
|
|
267
|
+
needContact: "Laissez un numéro de téléphone ou un e-mail pour que nous puissions vous joindre.",
|
|
268
|
+
noScript: "Réserver en ligne nécessite JavaScript — ou appelez-nous.",
|
|
269
|
+
},
|
|
52
270
|
};
|
|
53
271
|
|
|
54
272
|
// A letting is had for days, not at a time: «Час», «вільного часу немає» and
|
|
@@ -72,6 +290,46 @@ const DAYS = {
|
|
|
72
290
|
payNote: 'The days are held for 30 minutes. Complete the payment to keep them.',
|
|
73
291
|
taken: 'Those days have just been taken — please choose others.',
|
|
74
292
|
},
|
|
293
|
+
ru: {
|
|
294
|
+
day: "С",
|
|
295
|
+
pick: "Выберите даты",
|
|
296
|
+
time: "Дни",
|
|
297
|
+
none: "С этого дня ничего свободного нет.",
|
|
298
|
+
payNote: "Эти дни держим за вами 30 минут. Чтобы закрепить их, завершите оплату.",
|
|
299
|
+
taken: "Эти дни только что заняли — выберите другие.",
|
|
300
|
+
},
|
|
301
|
+
es: {
|
|
302
|
+
day: "Desde",
|
|
303
|
+
pick: "Elija las fechas",
|
|
304
|
+
time: "Días",
|
|
305
|
+
none: "A partir de ese día no queda nada libre.",
|
|
306
|
+
payNote: "Los días quedan retenidos 30 minutos. Complete el pago para conservarlos.",
|
|
307
|
+
taken: "Esos días acaban de ocuparse; elija otros, por favor.",
|
|
308
|
+
},
|
|
309
|
+
it: {
|
|
310
|
+
day: "Dal",
|
|
311
|
+
pick: "Scelga le date",
|
|
312
|
+
time: "Giorni",
|
|
313
|
+
none: "Niente di libero da quel giorno.",
|
|
314
|
+
payNote: "I giorni sono tenuti per 30 minuti. Completi il pagamento per mantenerli.",
|
|
315
|
+
taken: "Questi giorni sono appena stati presi: ne scelga altri.",
|
|
316
|
+
},
|
|
317
|
+
pt: {
|
|
318
|
+
day: "De",
|
|
319
|
+
pick: "Escolha as datas",
|
|
320
|
+
time: "Dias",
|
|
321
|
+
none: "Não há nada livre a partir desse dia.",
|
|
322
|
+
payNote: "Os dias ficam reservados durante 30 minutos. Conclua o pagamento para os manter.",
|
|
323
|
+
taken: "Esses dias acabaram de ser ocupados — escolha outros.",
|
|
324
|
+
},
|
|
325
|
+
fr: {
|
|
326
|
+
day: "Du",
|
|
327
|
+
pick: "Choisissez les dates",
|
|
328
|
+
time: "Jours",
|
|
329
|
+
none: "Rien n’est libre à partir de ce jour.",
|
|
330
|
+
payNote: "Les jours sont réservés pendant 30 minutes. Terminez le paiement pour les conserver.",
|
|
331
|
+
taken: "Ces jours viennent d’être pris — veuillez en choisir d’autres.",
|
|
332
|
+
},
|
|
75
333
|
};
|
|
76
334
|
|
|
77
335
|
/**
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The codes the annotation lint may say.
|
|
3
|
+
*
|
|
4
|
+
* A copy of the platform's `backend/api/sitebuild/errcodes.mjs`, which is
|
|
5
|
+
* generated from the one declaration of the codes (`registry/errcode.go`), for
|
|
6
|
+
* the same reason `codes.mjs` is a copy: a local lint that needs the network to
|
|
7
|
+
* name a fault is a local lint nobody runs. What keeps it honest is the
|
|
8
|
+
* platform's own parity test, which reads the lint it stages from this package
|
|
9
|
+
* against the Go table on every builder change.
|
|
10
|
+
*/
|
|
11
|
+
export const CODES = Object.freeze({
|
|
12
|
+
UNANNOTATED_FIELD: 'JTK_E_UNANNOTATED_FIELD',
|
|
13
|
+
PAGE_NOT_BUILT: 'JTK_E_PAGE_NOT_BUILT',
|
|
14
|
+
ROBOTS_DISALLOW: 'JTK_E_ROBOTS_DISALLOW',
|
|
15
|
+
SITEMAP_EMPTY: 'JTK_E_SITEMAP_EMPTY',
|
|
16
|
+
CANONICAL_ELSEWHERE: 'JTK_E_CANONICAL_ELSEWHERE',
|
|
17
|
+
NOINDEX: 'JTK_E_NOINDEX',
|
|
18
|
+
SHARED_NOT_RENDERED: 'JTK_E_SHARED_NOT_RENDERED',
|
|
19
|
+
ANNOTATION_UNKNOWN: 'JTK_E_ANNOTATION_UNKNOWN',
|
|
20
|
+
CATALOGUE_MISSING: 'JTK_E_CATALOGUE_MISSING',
|
|
21
|
+
FORM_INCOMPLETE: 'JTK_E_FORM_INCOMPLETE',
|
|
22
|
+
FORM_FIELD_UNDECLARED: 'JTK_E_FORM_FIELD_UNDECLARED',
|
|
23
|
+
FORM_UNDECLARED: 'JTK_E_FORM_UNDECLARED',
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
/** One line of the lint's output, with its code in front of it. */
|
|
27
|
+
export function said(code, message) {
|
|
28
|
+
return `${code} ${message}`;
|
|
29
|
+
}
|