opening_hours 3.13.0 → 3.14.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/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "type": "commonjs",
4
4
  "types": "./types/index.d.ts",
5
5
  "description": "Library to parse and process opening_hours tag from OpenStreetMap data",
6
- "version": "3.13.0",
6
+ "version": "3.14.0",
7
7
  "main": "./build/opening_hours.js",
8
8
  "module": "./build/opening_hours.esm.mjs",
9
9
  "sideEffects": false,
@@ -66,37 +66,39 @@
66
66
  "check-updates": "npm-check-updates --upgrade"
67
67
  },
68
68
  "dependencies": {
69
- "i18next": "^26.1.0",
70
- "suncalc": "^1.9.0"
69
+ "i18next": "^26.3.6",
70
+ "suncalc": "^2.0.0"
71
71
  },
72
72
  "devDependencies": {
73
- "@commitlint/cli": "^21.0.0",
74
- "@commitlint/config-conventional": "^21.0.0",
73
+ "@commitlint/cli": "^21.2.1",
74
+ "@commitlint/config-conventional": "^21.2.0",
75
75
  "@eslint/js": "^10.0.1",
76
- "@eslint/markdown": "^8.0.1",
77
- "@rollup/plugin-commonjs": "^29.0.2",
76
+ "@eslint/markdown": "^8.0.3",
77
+ "@rollup/plugin-commonjs": "^29.0.3",
78
78
  "@rollup/plugin-json": "^6.1.0",
79
79
  "@rollup/plugin-node-resolve": "^16.0.3",
80
80
  "@rollup/plugin-terser": "^1.0.0",
81
- "@rollup/plugin-yaml": "^4.1.2",
81
+ "@rollup/plugin-yaml": "^5.0.0",
82
82
  "@stylistic/eslint-plugin": "^5.10.0",
83
+ "cldr-dates-full": "^48.2.0",
84
+ "cldr-localenames-full": "^48.2.0",
83
85
  "commit-and-tag-version": "^12.7.3",
84
- "commitizen": "^4.3.1",
86
+ "commitizen": "^4.3.2",
85
87
  "cz-conventional-changelog": "^3.3.0",
86
- "doctoc": "^2.4.1",
87
- "eslint": "^10.3.0",
88
- "full-icu": "^1.5.0",
88
+ "doctoc": "^2.5.0",
89
+ "eslint": "^10.7.0",
90
+ "full-icu": "^1.6.0",
89
91
  "glob": "^13.0.6",
90
- "globals": "^17.6.0",
92
+ "globals": "^17.7.0",
91
93
  "husky": "^9.1.7",
92
- "lint-staged": "^17.0.4",
93
- "npm-check-updates": "^22.1.1",
94
+ "lint-staged": "^17.0.8",
95
+ "npm-check-updates": "^22.2.9",
94
96
  "package-json-validator-cli": "^0.1.11",
95
- "rollup": "^4.60.3",
96
- "terser": "^5.47.1",
97
+ "rollup": "^4.62.2",
98
+ "terser": "^5.49.0",
97
99
  "timekeeper": "^2.3.1",
98
100
  "typescript": "^6.0.3",
99
- "typescript-eslint": "^8.59.3",
101
+ "typescript-eslint": "^8.63.0",
100
102
  "yaml": "^2.9.0",
101
103
  "yargs": "^18.0.0"
102
104
  },
@@ -3,65 +3,100 @@
3
3
  *
4
4
  * SPDX-License-Identifier: LGPL-3.0-only
5
5
  */
6
- import opening_hours_resources from './opening_hours_resources.yaml';
6
+ import resources from './translations.yaml';
7
7
 
8
- const resources = opening_hours_resources;
9
-
10
- // Simple i18n object compatible with the minimal features used in src/index.js
11
- const i18n = {
12
- language: 'en',
13
- isInitialized: true,
14
-
15
- t: function(key, variables) {
16
- return this._translate(this.language, key, variables);
17
- },
18
-
19
- getFixedT: function(locale) {
20
- const self = this;
21
- return function(key, variables) {
22
- return self._translate(locale, key, variables);
23
- };
24
- },
25
-
26
- _translate: function(locale, key, variables) {
27
- // Handle array of keys (fallback mechanism)
28
- const keys = Array.isArray(key) ? key : [key];
8
+ /**
9
+ * Replace `{{varName}}` (or `{{-varName}}`) placeholders in a translation string.
10
+ * The `-` prefix is a legacy i18next no-HTML-escape marker; it has no effect here.
11
+ *
12
+ * @param {string} str - Translation string containing `{{…}}` placeholders.
13
+ * @param {Object} vars - Map of placeholder names to replacement values.
14
+ * @returns {string}
15
+ */
16
+ function interpolate(str, vars) {
17
+ return str.replace(/{{-?([^{}]*)}}/g, function(match, varName) {
18
+ const name = varName.trim();
19
+ if (name in vars) {
20
+ return vars[name];
21
+ }
22
+ return match;
23
+ });
24
+ }
29
25
 
30
- for (const k of keys) {
31
- // Parse namespace:path notation (e.g., "opening_hours:pretty.off")
32
- const parts = k.split(':');
33
- const namespace = parts.length > 1 ? parts[0] : 'opening_hours';
34
- const path = parts.length > 1 ? parts[1] : parts[0];
26
+ /**
27
+ * Extract the base language subtag from a locale tag (e.g. 'de-DE' → 'de').
28
+ *
29
+ * Accepts BCP 47 tags (e.g. 'de-DE') and POSIX identifiers per ISO 15897 (e.g. 'de_DE').
30
+ * For non-strings, returns 'en' (the default fallback locale).
31
+ *
32
+ * @param {string|null|undefined} locale - Locale tag.
33
+ * @returns {string} Base language, e.g. 'de'.
34
+ */
35
+ function baseLanguage(locale) {
36
+ if (locale && typeof locale === 'string') {
37
+ return locale.split(/[-_]/)[0];
38
+ }
39
+ return 'en';
40
+ }
35
41
 
36
- // Try to get translation
37
- const translation = this._getNestedValue(resources, [locale, namespace, ...path.split('.')]);
42
+ /**
43
+ * Build the locale fallback chain for a given locale tag.
44
+ *
45
+ * 'de-DE' → ['de-DE', 'de', 'en']
46
+ * 'de' → ['de', 'en']
47
+ * null → ['en']
48
+ *
49
+ * @param {string|null|undefined} locale - BCP 47 locale tag.
50
+ * @returns {string[]}
51
+ */
52
+ function localeChain(locale) {
53
+ const base = baseLanguage(locale);
54
+ // Deduplicate entries like ['de', 'de', 'en'] or ['en', 'en'].
55
+ return [...new Set([locale, base, 'en'].filter(Boolean))];
56
+ }
38
57
 
39
- if (translation !== undefined) {
40
- // Replace variables like {{variable}} or {{-variable}}
41
- // The minus prefix means "don't escape HTML" (compatibility feature)
42
- if (typeof translation === 'string' && variables) {
43
- return translation.replace(/{{-?([^{}]*)}}/g, function (match, varName) {
44
- const trimmed = varName.trim();
45
- return typeof variables[trimmed] !== 'undefined' ? variables[trimmed] : match;
46
- });
47
- }
48
- return translation;
49
- }
50
- }
58
+ /**
59
+ * Walk the resource tree for the first locale in the fallback chain that has
60
+ * a translation for the given key.
61
+ *
62
+ * Tree shape: resources[locale][section][…key segments]
63
+ *
64
+ * @param {string|null|undefined} locale - BCP 47 locale tag.
65
+ * @param {string} section - 'texts' or 'pretty'.
66
+ * @param {string} key - Translation key (dot-separated for nesting).
67
+ * @returns {*} Matched value, or `undefined` if not found anywhere in the chain.
68
+ */
69
+ function lookup(locale, section, key) {
70
+ const keyPath = key.split('.');
71
+ for (const loc of localeChain(locale)) {
72
+ let value = resources[loc]?.[section];
73
+ for (const segment of keyPath) value = value?.[segment];
74
+ // Return as soon as we find a value defined by the resource tree.
75
+ if (value !== undefined) return value;
76
+ }
77
+ return undefined;
78
+ }
51
79
 
52
- // Fallback: return the last key if no translation found
53
- const lastKey = keys[keys.length - 1];
54
- return lastKey.includes(':') ? lastKey.split(':')[1] : lastKey;
55
- },
80
+ /**
81
+ * Translate a key into the requested locale, falling back through the locale
82
+ * chain to English. Returns the key itself when no translation is found.
83
+ *
84
+ * @param {string|null|undefined} locale - BCP 47 locale tag (e.g. 'de', 'de-DE').
85
+ * @param {string} section - 'texts' (error/warning messages) or
86
+ * 'pretty' (prettified output tokens).
87
+ * @param {string} key - Translation key (dot-separated for nesting).
88
+ * @param {Object} [vars] - Variables to interpolate via `{{varName}}`.
89
+ * @returns {string}
90
+ */
91
+ export function translate(locale, section, key, vars) {
92
+ const result = lookup(locale, section, key);
56
93
 
57
- _getNestedValue: function(obj, path) {
58
- let current = obj;
59
- for (const key of path) {
60
- if (current === undefined || current === null) return undefined;
61
- current = current[key];
94
+ if (typeof result === 'string') {
95
+ if (vars) {
96
+ return interpolate(result, vars);
62
97
  }
63
- return current;
98
+ return result;
64
99
  }
65
- };
66
100
 
67
- export default i18n;
101
+ return key;
102
+ }
package/types/index.d.ts CHANGED
@@ -1,4 +1,45 @@
1
1
  declare module 'opening_hours' {
2
+ export type opening_hours_warning_type =
3
+ | 'adding_0'
4
+ | 'additional_rule_separator_not_used_after_time_wrapping_midnight'
5
+ | 'additional_rule_which_evaluates_to_closed'
6
+ | 'ambiguous_word'
7
+ | 'combine_rules'
8
+ | 'default_state'
9
+ | 'empty_comment'
10
+ | 'hour_min_separator'
11
+ | 'interpreted_as_year'
12
+ | 'no_colon_after'
13
+ | 'nothing_useful'
14
+ | 'omit_ko'
15
+ | 'period_one'
16
+ | 'period_one_year_plus'
17
+ | 'please_use_ok_for_ko'
18
+ | 'public_holiday'
19
+ | 'rant_degree_sign_used_for_zero'
20
+ | 'separator_for_readability'
21
+ | 'strange_24_7'
22
+ | 'switched'
23
+ | 'use_multi'
24
+ | 'vague'
25
+ | 'value_ends_with_token'
26
+ | 'without_minutes'
27
+ | 'word_error_correction'
28
+ | 'year_past'
29
+ | 'zero_calculation'
30
+
31
+ export interface opening_hours_warning {
32
+ type: opening_hours_warning_type
33
+ message: string
34
+ /** The string the `position` offset refers to. This is the input value,
35
+ * except for selector-reordering warnings where it is the prettified value. */
36
+ value: string
37
+ /** Character offset into `value` where the warning points (the spot the
38
+ * `<--- ` marker is placed at in `getWarnings()`). `null` if it could not
39
+ * be determined. */
40
+ position: number | null
41
+ }
42
+
2
43
  export class opening_hours {
3
44
  constructor(
4
45
  value: string,
@@ -31,6 +72,7 @@ declare module 'opening_hours' {
31
72
  date?: Date
32
73
  ): [boolean, Date, boolean, string | undefined, number | undefined]
33
74
  getWarnings(): string[]
75
+ getStructuredWarnings(): opening_hours_warning[]
34
76
  isEqualTo(
35
77
  second_oh_object: opening_hours,
36
78
  start_date?: Date