opening_hours 3.10.0 → 3.11.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.
@@ -1,13 +1,17 @@
1
- /* global i18next */
1
+ // Import dependencies
2
+ import i18next from '../../node_modules/i18next/dist/esm/i18next.bundled.js';
2
3
 
3
- // eslint-disable-next-line no-unused-vars
4
- const OpeningHoursTable = {
4
+ export const OpeningHoursTable = {
5
5
 
6
6
  // JS functions for generating the table {{{
7
7
  // In English. Localization is done somewhere else (above).
8
8
  months: ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'],
9
9
  weekdays: ['su', 'mo', 'tu', 'we', 'th', 'fr', 'sa'],
10
10
 
11
+ getLocalizedWeekday(date) {
12
+ return date.toLocaleString(i18next.language, { weekday: 'short' });
13
+ },
14
+
11
15
  formatdate (now, nextchange, from) {
12
16
  const now_daystart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
13
17
  const nextdays = (nextchange.getTime() - now_daystart.getTime()) / 1000 / 60 / 60 / 24;
@@ -85,8 +89,8 @@ const OpeningHoursTable = {
85
89
  return i18next.t(trans_base, {count: n});
86
90
  },
87
91
 
88
- printDate (date) {
89
- // return date.toLocaleDateString('en-CA');
92
+ toISODateString (date) {
93
+ // ISO 8601: https://xkcd.com/1179/
90
94
  return `${date.getFullYear()}-${
91
95
  this.pad(date.getMonth() + 1)}-${
92
96
  this.pad(date.getDate())}`;
@@ -99,34 +103,35 @@ const OpeningHoursTable = {
99
103
  this.pad(date.getSeconds())}`;
100
104
  },
101
105
 
102
- drawTable (it, date_today, has_next_change) {
106
+ drawTable (it, date_today, has_next_change, evalDate) {
103
107
  date_today = new Date(date_today);
104
108
  date_today.setHours(0, 0, 0, 0);
105
109
 
106
110
  const date = new Date(date_today);
107
- // date.setDate(date.getDate() - date.getDay() + 7);
108
111
  date.setDate(date.getDate() - date.getDay() - 1); // start at begin of the week
109
112
 
110
- const table = [];
113
+ // Calculate current time position for "now" marker (percentage of day)
114
+ // Use evalDate instead of new Date() to show the evaluation time, not browser time
115
+ const now = evalDate || new Date();
116
+ const nowPercent = ((now.getHours() * 60 + now.getMinutes()) / (24 * 60)) * 100;
117
+
118
+ const tableData = [];
111
119
 
112
120
  for (let row = 0; row < 7; row++) {
113
121
  date.setDate(date.getDate() + 1);
114
- // if (date.getDay() === date_today.getDay()) {
115
- // date.setDate(date.getDate()-7);
116
- // }
117
122
 
118
123
  it.setDate(date);
119
124
  let is_open = it.getState();
120
125
  let unknown = it.getUnknown();
121
- let state_string = it.getStateString(false);
126
+ let state_string = it.getStateString(true);
122
127
  let prevdate = date;
123
128
  let curdate = date;
124
- // console.log(state_string, is_open, unknown, date.toString());
125
129
 
126
- table[row] = {
130
+ const rowData = {
127
131
  date: new Date(date),
128
- times: '',
129
- text: []
132
+ times: [],
133
+ text: [],
134
+ isToday: date.getDay() === date_today.getDay()
130
135
  };
131
136
 
132
137
  while (has_next_change && it.advance() && curdate.getTime() - date.getTime() < 24 * 60 * 60 * 1000) {
@@ -142,55 +147,89 @@ const OpeningHoursTable = {
142
147
  fr *= 100 / 1000 / 60 / 60 / 24;
143
148
  to *= 100 / 1000 / 60 / 60 / 24;
144
149
 
145
- table[row].times += `<div class="timebar ${is_open ? 'open' : unknown ? 'unknown' : 'closed'
146
- }" style="width:${to - fr}%"></div>`;
147
- if (is_open || unknown) {
148
- let text = `${i18next.t(`words.${state_string}`)} ${
149
- i18next.t('words.from')} ${this.printTime(prevdate)
150
- } ${i18next.t('words.to')} `;
151
- if (prevdate.getDay() !== curdate.getDay()) {
152
- text += '24:00';
153
- } else {
154
- text += this.printTime(curdate);
155
- }
150
+ const stateClass = is_open ? 'open' : (unknown ? 'unknown' : 'closed');
151
+ // Always use 24h format with HH:MM
152
+ const timeFrom = `${String(prevdate.getHours()).padStart(2, '0')}:${String(prevdate.getMinutes()).padStart(2, '0')}`;
153
+ const timeToDate = prevdate.getDay() !== curdate.getDay() ? null : curdate;
154
+ const timeTo = timeToDate
155
+ ? `${String(timeToDate.getHours()).padStart(2, '0')}:${String(timeToDate.getMinutes()).padStart(2, '0')}`
156
+ : '24:00';
156
157
 
157
- table[row].text.push(text);
158
+ // Use current state_string for this period (before advancing)
159
+ const currentStateString = state_string;
160
+ const tooltip = `${i18next.t(`words.${currentStateString}`)}: ${timeFrom} - ${timeTo}`;
161
+
162
+ rowData.times.push(
163
+ `<div class="timebar ${stateClass}" style="width:${to - fr}%" title="${tooltip}"></div>`
164
+ );
165
+
166
+ if (is_open || unknown) {
167
+ const text = `${i18next.t(`words.${currentStateString}`)} ${i18next.t('words.from')} ${timeFrom} ${i18next.t('words.to')} ${timeTo}`;
168
+ rowData.text.push(text);
158
169
  }
159
170
 
160
171
  prevdate = curdate;
161
172
  is_open = it.getState();
162
173
  unknown = it.getUnknown();
163
- state_string = it.getStateString(false);
174
+ state_string = it.getStateString(true);
164
175
  }
165
176
 
166
- if (!has_next_change && table[row].text.length === 0) { // 24/7
167
- table[row].times += `<div class="timebar ${is_open ? 'open' : unknown ? 'unknown' : 'closed'
168
- }" style="width:100%"></div>`;
177
+ if (!has_next_change && rowData.text.length === 0) { // 24/7
178
+ const stateClass = is_open ? 'open' : (unknown ? 'unknown' : 'closed');
179
+ const tooltip = is_open ? `${i18next.t('words.open')}: 00:00 - 24:00` : '';
180
+ rowData.times.push(
181
+ `<div class="timebar ${stateClass}" style="width:100%" title="${tooltip}"></div>`
182
+ );
169
183
  if (is_open) {
170
- table[row].text.push(`${i18next.t('words.open')} 00:00 ${i18next.t('words.to')} 24:00`);
184
+ rowData.text.push(`${i18next.t('words.open')} 00:00 ${i18next.t('words.to')} 24:00`);
171
185
  }
172
186
  }
173
- }
174
187
 
175
- let output = '';
176
- output += '<table>';
177
- for (const row in table) {
178
- const today = table[row].date.getDay() === date_today.getDay();
179
- const endweek = (table[row].date.getDay() + 1) % 7 === date_today.getDay();
180
- const cl = today ? ' class="today"' : (endweek ? ' class="endweek"' : '');
181
-
182
- // if (today && date_today.getDay() !== 1)
183
- // output += '<tr class="separator"><td colspan="3"></td></tr>';
184
- output += `<tr${cl}><td class="day ${table[row].date.getDay() % 6 === 0 ? 'weekend' : 'workday'}">`;
185
- output += this.printDate(table[row].date);
186
- output += '</td><td class="times">';
187
- output += table[row].times;
188
- output += '</td><td>';
189
- output += table[row].text.join(', ') || '&nbsp;';
190
- output += '</td></tr>';
188
+ tableData.push(rowData);
191
189
  }
192
- output += '</table>';
193
- return output;
190
+
191
+ // Build table HTML
192
+ const headerRow = `
193
+ <tr class="time-scale">
194
+ <td></td>
195
+ <td>
196
+ <div class="scale-labels">
197
+ <span>0h</span>
198
+ <span>6h</span>
199
+ <span>12h</span>
200
+ <span>18h</span>
201
+ <span>24h</span>
202
+ </div>
203
+ </td>
204
+ <td></td>
205
+ </tr>`;
206
+
207
+ const rows = tableData.map(row => {
208
+ const isToday = row.date.getDay() === date_today.getDay();
209
+ const isEndWeek = (row.date.getDay() + 1) % 7 === date_today.getDay();
210
+ const rowClass = isToday ? ' class="today"' : (isEndWeek ? ' class="endweek"' : '');
211
+ const dayClass = row.date.getDay() % 6 === 0 ? 'weekend' : 'workday';
212
+ const weekdayName = this.getLocalizedWeekday(row.date);
213
+
214
+ // Add "now" marker for today
215
+ const nowMarker = isToday
216
+ ? `<div class="now-marker" style="left:${nowPercent}%" title="${i18next.t('words.time.now')}"></div>`
217
+ : '';
218
+
219
+ return `<tr${rowClass}>
220
+ <td class="day ${dayClass}">
221
+ <span class="weekday">${weekdayName}</span>
222
+ <span class="date">${this.toISODateString(row.date)}</span>
223
+ </td>
224
+ <td class="times">
225
+ ${row.times.join('')}
226
+ ${nowMarker}
227
+ </td>
228
+ <td class="description">${row.text.join(', ') || '&nbsp;'}</td>
229
+ </tr>`;
230
+ }).join('');
231
+
232
+ return `<table class="opening-hours-table">${headerRow}${rows}</table>`;
194
233
  },
195
234
 
196
235
  getReadableState (startString, endString, oh, past) {
@@ -201,43 +240,78 @@ const OpeningHoursTable = {
201
240
  return `${startString + output + endString}.`;
202
241
  },
203
242
 
204
- drawTableAndComments (oh, it, value) {
243
+ drawTableAndComments (oh, it, evalDate) {
205
244
  const prevdate = it.getDate();
206
245
  const unknown = it.getUnknown();
246
+ const currentState = it.getState();
207
247
  const state_string_past = it.getStateString(true);
208
248
  const comment = it.getComment();
209
249
  const has_next_change = it.advance();
210
250
 
211
251
  let output = '';
212
252
 
213
- output += `<p class="${state_string_past}">${
253
+ // 1. Current status
254
+ output += `<p class="${state_string_past} status-info">${
214
255
  i18next.t(`texts.${state_string_past} ${has_next_change ? 'now' : 'always'}`)}`;
215
- if (typeof comment !== 'undefined') {
216
- if (unknown) {
217
- output += i18next.t('texts.depends on', {comment: `"${comment}"`});
218
- } else {
219
- output += `, ${i18next.t('words.comment')}: "${comment}"`;
220
- }
256
+ if (unknown) {
257
+ output += i18next.t('texts.depends on', {comment: `"${comment}"`});
221
258
  }
222
259
  output += '</p>';
223
260
 
261
+ // 2. Show reason (comment) if present and not unknown
262
+ if (typeof comment !== 'undefined' && !unknown) {
263
+ output += `<p class="status-reason">↳ ${i18next.t('texts.reason')}: ${comment}</p>`;
264
+ }
265
+
266
+ // 3. Find next REAL state change (not just interval boundary)
224
267
  if (has_next_change) {
225
- let time_diff = it.getDate().getTime() - prevdate.getTime();
226
- time_diff /= 1000;
227
- time_diff += 60; // go one second after
228
- output += `<p class="${it.getStateString(true)}">${
229
- i18next.t(`texts.will ${it.getStateString(false)}`, {
230
- timestring: this.formatdate(prevdate, it.getDate(), true),
231
- href: `javascript:Evaluate(${time_diff}, false, '${value}')`,
232
- comment: typeof it.getComment() === 'string' || typeof comment === 'string'
233
- ? `, ${i18next.t('words.comment')}: ${typeof it.getComment() === 'string'
234
- ? `"${it.getComment()}"`
235
- : i18next.t('words.undefined')}`
236
- : ''
237
- })}</p>`;
268
+ let nextRealChangeDate = null;
269
+ let nextRealStateString = null;
270
+ let time_diff = 0;
271
+
272
+ // Check if immediate next change is a real state change
273
+ if (it.getState() !== currentState) {
274
+ nextRealChangeDate = it.getDate();
275
+ nextRealStateString = it.getStateString(false);
276
+ time_diff = (nextRealChangeDate.getTime() - prevdate.getTime()) / 1000 + 60;
277
+ } else {
278
+ // Keep advancing until we find a real state change
279
+ // Limit iterations to prevent infinite loops with complex values
280
+ const maxIterations = 1000;
281
+ let iterations = 0;
282
+ while (it.advance() && iterations < maxIterations) {
283
+ iterations++;
284
+ if (it.getState() !== currentState) {
285
+ nextRealChangeDate = it.getDate();
286
+ nextRealStateString = it.getStateString(false);
287
+ time_diff = (nextRealChangeDate.getTime() - prevdate.getTime()) / 1000 + 60;
288
+ break;
289
+ }
290
+ }
291
+ }
292
+
293
+ if (nextRealChangeDate) {
294
+ const timeString = this.formatdate(prevdate, nextRealChangeDate, true);
295
+ // Use "opens again" or "closes again" based on what will happen
296
+ const translationKey = nextRealStateString === 'open' ? 'texts.opens again' : 'texts.closes again';
297
+ const statusText = i18next.t(translationKey);
298
+ const buttonText = i18next.t('texts.jump to time');
299
+
300
+ const nextStateClass = nextRealStateString === 'open' ? 'opened' : 'closed';
301
+ output += `<p class="${nextStateClass} status-info next-change">
302
+ ${statusText}: ${timeString}
303
+ <a href="#" class="time-jump-btn" data-offset="${time_diff}" title="${buttonText}">
304
+ ${buttonText}
305
+ </a>
306
+ </p>`;
307
+ }
238
308
  }
239
309
 
240
- output += this.drawTable(it, prevdate, has_next_change);
310
+ // Add upcoming changes timeline
311
+ const upcomingChanges = this.generateUpcomingChanges(oh, evalDate, 5);
312
+ output += this.generateUpcomingChangesHTML(upcomingChanges, evalDate);
313
+
314
+ output += this.drawTable(it, prevdate, has_next_change, evalDate);
241
315
 
242
316
  if (oh.isWeekStable()) {
243
317
  output += `<p><b>${i18next.t('texts.week stable')}</b></p>`;
@@ -247,5 +321,90 @@ const OpeningHoursTable = {
247
321
 
248
322
  return output;
249
323
  },
324
+
325
+ // Generate upcoming changes timeline {{{
326
+ generateUpcomingChanges(oh, currentDate, maxChanges = 5) {
327
+ const changes = [];
328
+ const it = oh.getIterator(currentDate);
329
+ const currentState = it.getState();
330
+ let previousState = currentState;
331
+
332
+ // Collect next changes (all interval boundaries, not just state changes)
333
+ let count = 0;
334
+ while (count < maxChanges && it.advance()) {
335
+ const changeDate = it.getDate();
336
+ const newState = it.getState();
337
+ const comment = it.getComment();
338
+ const stateString = it.getStateString(true); // Use past form for consistency
339
+
340
+ changes.push({
341
+ date: changeDate,
342
+ state: newState,
343
+ stateString: stateString,
344
+ comment: comment,
345
+ isActualStateChange: previousState !== newState
346
+ });
347
+
348
+ previousState = newState;
349
+ count++;
350
+ }
351
+
352
+ return changes;
353
+ },
354
+
355
+ formatUpcomingChangeTime(currentDate, changeDate) {
356
+ const now_daystart = new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate());
357
+ const change_daystart = new Date(changeDate.getFullYear(), changeDate.getMonth(), changeDate.getDate());
358
+ const daysDiff = Math.round((change_daystart.getTime() - now_daystart.getTime()) / (1000 * 60 * 60 * 24));
359
+
360
+ // Always use 24h format with HH:MM
361
+ const hours = String(changeDate.getHours()).padStart(2, '0');
362
+ const minutes = String(changeDate.getMinutes()).padStart(2, '0');
363
+ const timeStr = `${hours}:${minutes}`;
364
+
365
+ if (daysDiff === 0) {
366
+ return `${i18next.t('words.today')} ${timeStr}`;
367
+ } else if (daysDiff === 1) {
368
+ return `${i18next.t('words.tomorrow')} ${timeStr}`;
369
+ } else if (daysDiff === -1) {
370
+ return `${i18next.t('words.yesterday')} ${timeStr}`;
371
+ } else {
372
+ // For dates further away, show date + time
373
+ const dateStr = this.toISODateString(changeDate);
374
+ return `${dateStr} ${timeStr}`;
375
+ }
376
+ },
377
+
378
+ generateUpcomingChangesHTML(changes, currentDate) {
379
+ if (changes.length === 0) return '';
380
+
381
+ let html = `<details class="upcoming-changes">
382
+ <summary>${i18next.t('texts.interval boundaries')}</summary>
383
+ <p class="timeline-hint">${i18next.t('texts.interval boundaries hint')}</p>
384
+ <ul class="timeline">`;
385
+
386
+ for (const change of changes) {
387
+ const timeStr = this.formatUpcomingChangeTime(currentDate, change.date);
388
+ const stateClass = change.state ? 'opened' : 'closed';
389
+ // Visual distinction: filled circle for real changes, empty for boundaries
390
+ const changeIcon = change.isActualStateChange ? '●' : '○';
391
+ const changeType = change.isActualStateChange ? 'state-change' : 'boundary-only';
392
+ const stateText = i18next.t(`words.${change.stateString}`);
393
+ const commentText = typeof change.comment === 'string'
394
+ ? ` <span class="timeline-comment">(${change.comment})</span>`
395
+ : '';
396
+
397
+ html += `<li class="timeline-item ${stateClass} ${changeType}">
398
+ <span class="timeline-icon">${changeIcon}</span>
399
+ <span class="timeline-time">${timeStr}</span>
400
+ <span class="timeline-arrow">→</span>
401
+ <span class="timeline-state">${stateText}</span>${commentText}
402
+ </li>`;
403
+ }
404
+
405
+ html += '</ul></details>';
406
+ return html;
407
+ },
408
+ // }}}
250
409
  // }}}
251
410
  };
@@ -0,0 +1,70 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: © 2025 Kristjan ESPERANTO <https://github.com/KristjanESPERANTO>
3
+ *
4
+ * SPDX-License-Identifier: LGPL-3.0-only
5
+ */
6
+
7
+ // Theme management: localStorage -> browser preference -> fallback (light)
8
+ (function() {
9
+ 'use strict';
10
+
11
+ const STORAGE_KEY = 'theme-preference';
12
+
13
+ function getThemePreference() {
14
+ // 1. Check localStorage
15
+ const stored = localStorage.getItem(STORAGE_KEY);
16
+ if (stored) {
17
+ return stored;
18
+ }
19
+
20
+ // 2. Check browser preference
21
+ if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
22
+ return 'dark';
23
+ }
24
+ if (window.matchMedia('(prefers-color-scheme: light)').matches) {
25
+ return 'light';
26
+ }
27
+
28
+ // 3. Fallback to light
29
+ return 'light';
30
+ }
31
+
32
+ function setTheme(theme) {
33
+ document.body.setAttribute('data-theme', theme);
34
+ localStorage.setItem(STORAGE_KEY, theme);
35
+ }
36
+
37
+ function toggleTheme() {
38
+ const currentTheme = document.body.getAttribute('data-theme') || getThemePreference();
39
+ const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
40
+ setTheme(newTheme);
41
+ }
42
+
43
+ // Initialize theme immediately to avoid FOUC
44
+ const theme = getThemePreference();
45
+ setTheme(theme);
46
+
47
+ // Set up toggle button when DOM is ready
48
+ if (document.readyState === 'loading') {
49
+ document.addEventListener('DOMContentLoaded', initToggleButton);
50
+ } else {
51
+ initToggleButton();
52
+ }
53
+
54
+ function initToggleButton() {
55
+ const toggleBtn = document.getElementById('theme-toggle');
56
+ if (toggleBtn) {
57
+ toggleBtn.addEventListener('click', toggleTheme);
58
+ }
59
+
60
+ // Listen for system theme changes (only if no localStorage preference)
61
+ if (!localStorage.getItem(STORAGE_KEY)) {
62
+ const darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)');
63
+ darkModeQuery.addEventListener('change', (e) => {
64
+ if (!localStorage.getItem(STORAGE_KEY)) {
65
+ setTheme(e.matches ? 'dark' : 'light');
66
+ }
67
+ });
68
+ }
69
+ }
70
+ })();
@@ -29,7 +29,7 @@
29
29
  /**
30
30
  * The days in a week
31
31
  */
32
- DAYS = {
32
+ const DAYS = {
33
33
  MONDAY: 0,
34
34
  TUESDAY: 1,
35
35
  WEDNESDAY: 2,
@@ -42,42 +42,42 @@ DAYS = {
42
42
  /**
43
43
  * The days in OSM
44
44
  */
45
- OSM_DAYS = [ "Mo", "Tu", "We", "Th", "Fr", "Sa", "Su" ];
45
+ const OSM_DAYS = [ "Mo", "Tu", "We", "Th", "Fr", "Sa", "Su" ];
46
46
 
47
47
  /**
48
48
  * The days IRL
49
49
  */
50
- IRL_DAYS = [ "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" ];
50
+ const IRL_DAYS = [ "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" ];
51
51
 
52
52
  /**
53
53
  * The month in OSM
54
54
  */
55
- OSM_MONTHS = [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ];
55
+ const OSM_MONTHS = [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ];
56
56
 
57
57
  /**
58
58
  * The months IRL
59
59
  */
60
- IRL_MONTHS = [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ];
60
+ const IRL_MONTHS = [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ];
61
61
 
62
62
  /**
63
63
  * The last day of month
64
64
  */
65
- MONTH_END_DAY = [ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ];
65
+ const MONTH_END_DAY = [ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ];
66
66
 
67
67
  /**
68
68
  * The maximal minute that an interval can have
69
69
  */
70
- MINUTES_MAX = 1440;
70
+ const MINUTES_MAX = 1440;
71
71
 
72
72
  /**
73
73
  * The maximal value of days
74
74
  */
75
- DAYS_MAX = 6;
75
+ const DAYS_MAX = 6;
76
76
 
77
77
  /**
78
78
  * The weekday ID for PH
79
79
  */
80
- PH_WEEKDAY = -2;
80
+ const PH_WEEKDAY = -2;
81
81
 
82
82
  /*
83
83
  * ========== CLASSES ==========
@@ -717,7 +717,7 @@ var Day = function() {
717
717
  this._intervals = [];
718
718
  for(var i=0; i < intervals.length; i++) {
719
719
  if(intervals[i] != undefined && intervals[i].getStartDay() == 0 && intervals[i].getEndDay() == 0) {
720
- this._intervals.push($.extend(true, {}, intervals[i]));
720
+ this._intervals.push(structuredClone(intervals[i]));
721
721
  }
722
722
  }
723
723
 
@@ -1045,7 +1045,7 @@ var Week = function() {
1045
1045
  this._intervals = [];
1046
1046
  for(var i=0; i < intervals.length; i++) {
1047
1047
  if(intervals[i] != undefined) {
1048
- this._intervals.push($.extend(true, {}, intervals[i]));
1048
+ this._intervals.push(structuredClone(intervals[i]));
1049
1049
  }
1050
1050
  }
1051
1051
  };
@@ -2177,7 +2177,7 @@ var OpeningHoursParser = function() {
2177
2177
 
2178
2178
  var block, tokens, currentToken, ruleModifier, timeSelector, weekdaySelector, wideRangeSelector;
2179
2179
  var singleTime, from, to, times;
2180
- var singleWeekday, wdStart, wdEnd, holidays, weekdays;
2180
+ var singleWeekday, wdStart, wdEnd, wdFrom, wdTo, holidays, weekdays;
2181
2181
  var monthSelector, weekSelector, weeks, singleWeek, weekFrom, weekTo, singleMonth, months, monthFrom, monthTo;
2182
2182
  var dateRanges, dateRange, drObj, foundDateRange, resDrId;
2183
2183
 
@@ -2638,7 +2638,7 @@ var OpeningHoursParser = function() {
2638
2638
  //Check added interval are OK for days
2639
2639
  if(typical instanceof Day) {
2640
2640
  if(weekdays.from != 0 || (weekdays.to != 0 && times.from <= times.to)) {
2641
- weekdays = $.extend({}, weekdays);
2641
+ weekdays = Object.assign({}, weekdays);
2642
2642
  weekdays.from = 0;
2643
2643
  weekdays.to = (times.from <= times.to) ? 0 : 1;
2644
2644
  }
@@ -2728,10 +2728,10 @@ var OpeningHoursParser = function() {
2728
2728
  */
2729
2729
  OpeningHoursParser.prototype._tokenize = function(block) {
2730
2730
  var result = block.trim().split(' ');
2731
- var position = $.inArray("", result);
2731
+ var position = result.indexOf("");
2732
2732
  while( ~position ) {
2733
2733
  result.splice(position, 1);
2734
- position = $.inArray("", result);
2734
+ position = result.indexOf("");
2735
2735
  }
2736
2736
  return result;
2737
2737
  };
@@ -2784,3 +2784,5 @@ var YoHoursChecker = function() {
2784
2784
 
2785
2785
  return result;
2786
2786
  };
2787
+
2788
+ export { OpeningHoursBuilder, OpeningHoursParser, YoHoursChecker };
@@ -0,0 +1,62 @@
1
+ import opening_hours_resources from './opening_hours_resources.yaml';
2
+
3
+ const resources = opening_hours_resources;
4
+
5
+ // Simple i18n object compatible with the minimal features used in src/index.js
6
+ const i18n = {
7
+ language: 'en',
8
+ isInitialized: true,
9
+
10
+ t: function(key, variables) {
11
+ return this._translate(this.language, key, variables);
12
+ },
13
+
14
+ getFixedT: function(locale) {
15
+ const self = this;
16
+ return function(key, variables) {
17
+ return self._translate(locale, key, variables);
18
+ };
19
+ },
20
+
21
+ _translate: function(locale, key, variables) {
22
+ // Handle array of keys (fallback mechanism)
23
+ const keys = Array.isArray(key) ? key : [key];
24
+
25
+ for (const k of keys) {
26
+ // Parse namespace:path notation (e.g., "opening_hours:pretty.off")
27
+ const parts = k.split(':');
28
+ const namespace = parts.length > 1 ? parts[0] : 'opening_hours';
29
+ const path = parts.length > 1 ? parts[1] : parts[0];
30
+
31
+ // Try to get translation
32
+ const translation = this._getNestedValue(resources, [locale, namespace, ...path.split('.')]);
33
+
34
+ if (translation !== undefined) {
35
+ // Replace variables like {{variable}} or {{-variable}}
36
+ // The minus prefix means "don't escape HTML" (compatibility feature)
37
+ if (typeof translation === 'string' && variables) {
38
+ return translation.replace(/{{-?([^{}]*)}}/g, function (match, varName) {
39
+ const trimmed = varName.trim();
40
+ return typeof variables[trimmed] !== 'undefined' ? variables[trimmed] : match;
41
+ });
42
+ }
43
+ return translation;
44
+ }
45
+ }
46
+
47
+ // Fallback: return the last key if no translation found
48
+ const lastKey = keys[keys.length - 1];
49
+ return lastKey.includes(':') ? lastKey.split(':')[1] : lastKey;
50
+ },
51
+
52
+ _getNestedValue: function(obj, path) {
53
+ let current = obj;
54
+ for (const key of path) {
55
+ if (current === undefined || current === null) return undefined;
56
+ current = current[key];
57
+ }
58
+ return current;
59
+ }
60
+ };
61
+
62
+ export default i18n;
@@ -1,20 +0,0 @@
1
- import i18next from 'i18next';
2
- export default i18next;
3
-
4
- import opening_hours_resources from './opening_hours_resources.yaml';
5
-
6
- if (!i18next.isInitialized) {
7
- i18next.init({
8
- fallbackLng: 'en',
9
- // lngWhitelist: ['en', 'de'],
10
- resources: opening_hours_resources,
11
- getAsync: true,
12
- useCookie: true,
13
- // debug: true,
14
- });
15
- } else {
16
- // compat with an app that already initializes i18n
17
- for (const lang in opening_hours_resources) {
18
- i18next.addResourceBundle(lang, 'opening_hours', opening_hours_resources[lang]['opening_hours'], true);
19
- }
20
- }