@schukai/monster 3.96.3 → 3.97.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.
Files changed (100) hide show
  1. package/CHANGELOG.md +15 -96
  2. package/package.json +1 -1
  3. package/source/components/accessibility/locale-picker.mjs +598 -0
  4. package/source/components/accessibility/style/locale-picker.css +1 -0
  5. package/source/components/accessibility/style/locale-picker.pcss +26 -0
  6. package/source/components/accessibility/stylesheet/locale-picker.mjs +31 -0
  7. package/source/components/content/stylesheet/copy.mjs +2 -2
  8. package/source/components/datatable/stylesheet/change-button.mjs +2 -2
  9. package/source/components/datatable/stylesheet/column-bar.mjs +2 -2
  10. package/source/components/datatable/stylesheet/dataset.mjs +2 -2
  11. package/source/components/datatable/stylesheet/datasource.mjs +1 -1
  12. package/source/components/datatable/stylesheet/datatable.mjs +1 -1
  13. package/source/components/datatable/stylesheet/embedded-pagination.mjs +2 -2
  14. package/source/components/datatable/stylesheet/filter-button.mjs +1 -1
  15. package/source/components/datatable/stylesheet/filter-controls-defaults.mjs +1 -1
  16. package/source/components/datatable/stylesheet/filter-date-range.mjs +1 -1
  17. package/source/components/datatable/stylesheet/filter-range.mjs +1 -1
  18. package/source/components/datatable/stylesheet/filter-select.mjs +2 -2
  19. package/source/components/datatable/stylesheet/filter.mjs +2 -2
  20. package/source/components/datatable/stylesheet/pagination.mjs +1 -1
  21. package/source/components/datatable/stylesheet/save-button.mjs +2 -2
  22. package/source/components/datatable/stylesheet/status.mjs +1 -1
  23. package/source/components/form/stylesheet/action-button.mjs +1 -1
  24. package/source/components/form/stylesheet/api-bar.mjs +1 -1
  25. package/source/components/form/stylesheet/api-button.mjs +1 -1
  26. package/source/components/form/stylesheet/button-bar.mjs +1 -1
  27. package/source/components/form/stylesheet/button.mjs +1 -1
  28. package/source/components/form/stylesheet/confirm-button.mjs +1 -1
  29. package/source/components/form/stylesheet/context-error.mjs +1 -1
  30. package/source/components/form/stylesheet/context-help.mjs +1 -1
  31. package/source/components/form/stylesheet/field-set.mjs +1 -1
  32. package/source/components/form/stylesheet/form.mjs +1 -1
  33. package/source/components/form/stylesheet/input-group.mjs +1 -1
  34. package/source/components/form/stylesheet/message-state-button.mjs +1 -1
  35. package/source/components/form/stylesheet/password.mjs +1 -1
  36. package/source/components/form/stylesheet/popper-button.mjs +1 -1
  37. package/source/components/form/stylesheet/select.mjs +1 -1
  38. package/source/components/form/stylesheet/state-button.mjs +1 -1
  39. package/source/components/form/stylesheet/toggle-switch.mjs +1 -1
  40. package/source/components/form/stylesheet/tree-select.mjs +1 -1
  41. package/source/components/host/stylesheet/call-button.mjs +2 -2
  42. package/source/components/host/stylesheet/config-manager.mjs +1 -1
  43. package/source/components/host/stylesheet/host.mjs +2 -2
  44. package/source/components/host/stylesheet/overlay.mjs +2 -2
  45. package/source/components/host/stylesheet/toggle-button.mjs +2 -2
  46. package/source/components/host/stylesheet/viewer.mjs +2 -2
  47. package/source/components/layout/stylesheet/collapse.mjs +2 -2
  48. package/source/components/layout/stylesheet/details.mjs +2 -2
  49. package/source/components/layout/stylesheet/iframe.mjs +1 -1
  50. package/source/components/layout/stylesheet/panel.mjs +2 -2
  51. package/source/components/layout/stylesheet/popper.mjs +1 -1
  52. package/source/components/layout/stylesheet/slider.mjs +2 -2
  53. package/source/components/layout/stylesheet/split-panel.mjs +1 -1
  54. package/source/components/layout/stylesheet/tabs.mjs +2 -2
  55. package/source/components/layout/stylesheet/width-toggle.mjs +1 -1
  56. package/source/components/navigation/stylesheet/table-of-content.mjs +2 -2
  57. package/source/components/notify/stylesheet/message.mjs +2 -2
  58. package/source/components/notify/stylesheet/notify.mjs +1 -1
  59. package/source/components/state/stylesheet/log.mjs +1 -1
  60. package/source/components/state/stylesheet/state.mjs +1 -1
  61. package/source/components/style/property.css +1 -0
  62. package/source/components/style/theme.css +4 -4
  63. package/source/components/stylesheet/badge.mjs +1 -1
  64. package/source/components/stylesheet/border.mjs +1 -1
  65. package/source/components/stylesheet/button.mjs +1 -1
  66. package/source/components/stylesheet/card.mjs +1 -1
  67. package/source/components/stylesheet/color.mjs +1 -1
  68. package/source/components/stylesheet/common.mjs +1 -1
  69. package/source/components/stylesheet/control.mjs +1 -1
  70. package/source/components/stylesheet/data-grid.mjs +1 -1
  71. package/source/components/stylesheet/display.mjs +1 -1
  72. package/source/components/stylesheet/floating-ui.mjs +1 -1
  73. package/source/components/stylesheet/form.mjs +1 -1
  74. package/source/components/stylesheet/host.mjs +1 -1
  75. package/source/components/stylesheet/icons.mjs +1 -1
  76. package/source/components/stylesheet/link.mjs +1 -1
  77. package/source/components/stylesheet/mixin/badge.mjs +1 -1
  78. package/source/components/stylesheet/mixin/button.mjs +1 -1
  79. package/source/components/stylesheet/mixin/hover.mjs +1 -1
  80. package/source/components/stylesheet/mixin/icon.mjs +1 -1
  81. package/source/components/stylesheet/mixin/media.mjs +1 -1
  82. package/source/components/stylesheet/mixin/property.mjs +1 -1
  83. package/source/components/stylesheet/mixin/skeleton.mjs +1 -1
  84. package/source/components/stylesheet/mixin/spinner.mjs +1 -1
  85. package/source/components/stylesheet/mixin/typography.mjs +1 -1
  86. package/source/components/stylesheet/normalize.mjs +1 -1
  87. package/source/components/stylesheet/popper.mjs +1 -1
  88. package/source/components/stylesheet/property.mjs +2 -2
  89. package/source/components/stylesheet/ripple.mjs +1 -1
  90. package/source/components/stylesheet/skeleton.mjs +1 -1
  91. package/source/components/stylesheet/space.mjs +1 -1
  92. package/source/components/stylesheet/spinner.mjs +1 -1
  93. package/source/components/stylesheet/table.mjs +1 -1
  94. package/source/components/stylesheet/theme.mjs +1 -1
  95. package/source/components/stylesheet/typography.mjs +1 -1
  96. package/source/components/tree-menu/stylesheet/tree-menu.mjs +1 -1
  97. package/source/i18n/locale.mjs +151 -151
  98. package/source/i18n/map/languages.mjs +104 -0
  99. package/source/i18n/util.mjs +139 -0
  100. package/test/cases/i18n/util.mjs +295 -0
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Copyright © schukai GmbH and all contributing authors, {{copyRightYear}}. All rights reserved.
3
+ * Node module: @schukai/monster
4
+ *
5
+ * This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3).
6
+ * The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html
7
+ *
8
+ * For those who do not wish to adhere to the AGPLv3, a commercial license is available.
9
+ * Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms.
10
+ * For more information about purchasing a commercial license, please contact schukai GmbH.
11
+ *
12
+ * SPDX-License-Identifier: AGPL-3.0
13
+ */
14
+
15
+ export const languages = {
16
+ "en": "English",
17
+ "en-GB": "English (United Kingdom)",
18
+ "en-US": "English (United States)",
19
+ "en-CA": "English (Canada)",
20
+ "en-AU": "English (Australia)",
21
+ "es": "Español",
22
+ "es-ES": "Español (España)",
23
+ "es-MX": "Español (México)",
24
+ "es-AR": "Español (Argentina)",
25
+ "zh": "中文",
26
+ "zh-CN": "中文(简体)",
27
+ "zh-TW": "中文(繁體)",
28
+ "zh-HK": "中文(香港)",
29
+ "fr": "Français",
30
+ "fr-FR": "Français (France)",
31
+ "fr-CA": "Français (Canada)",
32
+ "fr-BE": "Français (Belgique)",
33
+ "de": "Deutsch",
34
+ "de-DE": "Deutsch (Deutschland)",
35
+ "de-AT": "Deutsch (Österreich)",
36
+ "de-CH": "Deutsch (Schweiz)",
37
+ "ja": "日本語",
38
+ "ru": "Русский",
39
+ "ru-RU": "Русский (Россия)",
40
+ "it": "Italiano",
41
+ "it-IT": "Italiano (Italia)",
42
+ "pt": "Português",
43
+ "pt-PT": "Português (Portugal)",
44
+ "pt-BR": "Português (Brasil)",
45
+ "ko": "한국어",
46
+ "ar": "العربية",
47
+ "hi": "हिन्दी",
48
+ "bn": "বাংলা",
49
+ "pa": "ਪੰਜਾਬੀ",
50
+ "id": "Bahasa Indonesia",
51
+ "vi": "Tiếng Việt",
52
+ "tr": "Türkçe",
53
+ "tr-TR": "Türkçe (Türkiye)",
54
+ "pl": "Polski",
55
+ "pl-PL": "Polski (Polska)",
56
+ "uk": "Українська",
57
+ "uk-UA": "Українська (Україна)",
58
+ "ro": "Română",
59
+ "ro-RO": "Română (România)",
60
+ "nl": "Nederlands",
61
+ "nl-NL": "Nederlands (Nederland)",
62
+ "el": "Ελληνικά",
63
+ "el-GR": "Ελληνικά (Ελλάδα)",
64
+ "hu": "Magyar",
65
+ "hu-HU": "Magyar (Magyarország)",
66
+ "sv": "Svenska",
67
+ "sv-SE": "Svenska (Sverige)",
68
+ "cs": "Čeština",
69
+ "cs-CZ": "Čeština (Česká republika)",
70
+ "bg": "Български",
71
+ "bg-BG": "Български (България)",
72
+ "da": "Dansk",
73
+ "da-DK": "Dansk (Danmark)",
74
+ "fi": "Suomi",
75
+ "fi-FI": "Suomi (Suomi)",
76
+ "sk": "Slovenčina",
77
+ "sk-SK": "Slovenčina (Slovensko)",
78
+ "he": "עברית",
79
+ "th": "ไทย",
80
+ "sr": "Српски",
81
+ "sr-RS": "Српски (Србија)",
82
+ "no": "Norsk",
83
+ "no-NO": "Norsk (Norge)",
84
+ "lt": "Lietuvių",
85
+ "lt-LT": "Lietuvių (Lietuva)",
86
+ "lv": "Latviešu",
87
+ "lv-LV": "Latviešu (Latvija)",
88
+ "et": "Eesti",
89
+ "et-EE": "Eesti (Eesti)",
90
+ "hr": "Hrvatski",
91
+ "hr-HR": "Hrvatski (Hrvatska)",
92
+ "sl": "Slovenščina",
93
+ "sl-SI": "Slovenščina (Slovenija)",
94
+ "mt": "Malti",
95
+ "mt-MT": "Malti (Malta)",
96
+ "is": "Íslenska",
97
+ "is-IS": "Íslenska (Ísland)",
98
+ "ga": "Gaeilge",
99
+ "ga-IE": "Gaeilge (Éire)",
100
+ "cy": "Cymraeg",
101
+ "cy-GB": "Cymraeg (Y Deyrnas Unedig)",
102
+ "sq": "Shqip",
103
+ "sq-AL": "Shqip (Shqipëria)"
104
+ };
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Copyright © schukai GmbH and all contributing authors, {{copyRightYear}}. All rights reserved.
3
+ * Node module: @schukai/monster
4
+ *
5
+ * This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3).
6
+ * The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html
7
+ *
8
+ * For those who do not wish to adhere to the AGPLv3, a commercial license is available.
9
+ * Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms.
10
+ * For more information about purchasing a commercial license, please contact schukai GmbH.
11
+ *
12
+ * SPDX-License-Identifier: AGPL-3.0
13
+ */
14
+
15
+ import {languages} from "./map/languages.mjs";
16
+
17
+ /**
18
+ * Determines the user's preferred language based on browser settings and available language options.
19
+ *
20
+ * It evaluates the current HTML document language, the browser's defined languages, and
21
+ * the language options from `<link>` elements with `hreflang` attributes in the document.
22
+ *
23
+ * @return {Object} An object containing information about the detected language, preferred language, and available languages.
24
+ */
25
+ export function detectUserLanguagePreference() {
26
+ const currentLang = document.documentElement.lang;
27
+
28
+ let preferredLanguages = [];
29
+
30
+ if (typeof navigator.language === "string" && navigator.language.length > 0) {
31
+ preferredLanguages = [navigator.language];
32
+ }
33
+
34
+ if (Array.isArray(navigator.languages) && navigator.languages.length > 0) {
35
+ preferredLanguages = navigator.languages;
36
+ }
37
+
38
+ // add to preferredLanguages all the base languages of the preferred languages
39
+ preferredLanguages = preferredLanguages.concat(preferredLanguages.map(lang => lang.split("-")[0]));
40
+
41
+
42
+ if (!currentLang && preferredLanguages.length === 0) {
43
+ return {
44
+ message: "No language information available.",
45
+ };
46
+ }
47
+
48
+ const linkTags = document.querySelectorAll("link[hreflang]");
49
+ if (linkTags.length === 0) {
50
+ return {
51
+ current: currentLang || null,
52
+ message: "No <link> tags with hreflang available.",
53
+ };
54
+ }
55
+
56
+ const availableLanguages = [...linkTags].map((link) => {
57
+ const fullLang = link.hreflang;
58
+ const baseLang = fullLang.split("-")[0];
59
+ let label = link.getAttribute('data-monster-label')
60
+ if (!label) {
61
+ label = languages?.[fullLang]
62
+ if (!label) {
63
+ label = languages?.[baseLang]
64
+ }
65
+ }
66
+
67
+ return {
68
+ fullLang,
69
+ baseLang,
70
+ label,
71
+ href: link.href,
72
+ };
73
+ });
74
+
75
+ // filter availableLanguages to only include languages that are in the preferredLanguages array
76
+ const offerableLanguages = availableLanguages.filter(lang => preferredLanguages.includes(lang.fullLang) || preferredLanguages.includes(lang.baseLang));
77
+
78
+ if (offerableLanguages.length === 0) {
79
+ return {
80
+ current: currentLang || null,
81
+ message: "No available languages match the user's preferences.",
82
+ available: availableLanguages.map((lang) => ({
83
+ ...lang,
84
+ weight: 1,
85
+ })),
86
+ };
87
+ }
88
+
89
+ // Helper function to determine the "weight" of a language match
90
+ function getWeight(langEntry) {
91
+ // Full match has priority 3
92
+ if (preferredLanguages.includes(langEntry.fullLang)) return 3;
93
+ // Base language match has priority 2
94
+ if (preferredLanguages.includes(langEntry.baseLang)) return 2;
95
+ // No match is priority 1
96
+ return 1;
97
+ }
98
+
99
+ // Sort the available languages by descending weight
100
+ offerableLanguages.sort((a, b) => getWeight(b) - getWeight(a));
101
+
102
+ // The best match is the first in the sorted list
103
+ const bestMatch = offerableLanguages[0];
104
+ const bestMatchWeight = getWeight(bestMatch);
105
+
106
+ const currentLabel = languages?.[currentLang] || currentLang
107
+
108
+ // If we found a language that matches user preferences (weight > 1)
109
+ if (bestMatchWeight > 0) {
110
+ return {
111
+ current: currentLang || null,
112
+ currentLabel: currentLabel,
113
+ preferred: {
114
+ full: bestMatch.fullLang,
115
+ base: bestMatch.baseLang,
116
+ label: bestMatch.label,
117
+ href : bestMatch.href,
118
+ },
119
+ available: availableLanguages.map((lang) => ({
120
+ ...lang,
121
+ weight: getWeight(lang),
122
+ })),
123
+ offerable: offerableLanguages.map((lang) => ({
124
+ ...lang,
125
+ weight: getWeight(lang),
126
+ })),
127
+ };
128
+ }
129
+
130
+ // If no language matched the user's preferences
131
+ return {
132
+ current: currentLang || null,
133
+ message: "None of the preferred languages are available.",
134
+ available: availableLanguages.map((lang) => ({
135
+ ...lang,
136
+ weight: getWeight(lang),
137
+ })),
138
+ };
139
+ }
@@ -0,0 +1,295 @@
1
+ import {getGlobal} from "../../../source/types/global.mjs";
2
+
3
+ import * as chai from 'chai';
4
+ import {chaiDom} from "../../util/chai-dom.mjs";
5
+ import {initJSDOM} from "../../util/jsdom.mjs";
6
+
7
+ let expect = chai.expect;
8
+ chai.use(chaiDom);
9
+
10
+ describe('LocalPicker', function () {
11
+
12
+ let LocalPicker, documentLanguage, linkTags, originalLanguages, getPreferredLanguage;
13
+
14
+ before(function (done) {
15
+ initJSDOM().then(() => {
16
+
17
+ documentLanguage = document.documentElement.lang;
18
+ linkTags = Array.from(document.querySelectorAll('link[rel="alternate"]'));
19
+ linkTags.forEach(item => item.remove());
20
+
21
+ import("element-internals-polyfill").catch(e => done(e));
22
+
23
+ import("../../../source/i18n/util.mjs").then((m) => {
24
+
25
+ LocalPicker = m['LocalPicker'];
26
+ getPreferredLanguage = m['detectUserLanguagePreference'];
27
+
28
+ originalLanguages = navigator.languages;
29
+
30
+ done()
31
+ }).catch(e => done(e))
32
+
33
+
34
+ });
35
+ })
36
+
37
+ after(function () {
38
+ document.documentElement.lang = documentLanguage;
39
+
40
+ linkTags.forEach(item => {
41
+ const link = document.createElement('link');
42
+ link.setAttribute('rel', 'alternate');
43
+ link.setAttribute('hreflang', item.hreflang);
44
+ link.setAttribute('href', item.href);
45
+
46
+ // check if already exits
47
+ if (!document.querySelector(`link[hreflang="${item.hreflang}"]`)) {
48
+ document.querySelector('head').appendChild(link);
49
+ }
50
+ });
51
+
52
+
53
+ Object.defineProperty(navigator, 'languages', {
54
+ value: originalLanguages,
55
+ writable: true
56
+ });
57
+
58
+
59
+ })
60
+
61
+ // Hilfsfunktion zum Setup eines JSDOM-Dokuments mit <html> und ggf. link-Tags
62
+ function setupDOM({htmlLang = '', linkHreflangs = [], navLang = ""}) {
63
+
64
+ linkTags = Array.from(document.querySelectorAll('link[rel="alternate"]'));
65
+ linkTags.forEach(item => item.remove());
66
+
67
+ // Links hinzufügen
68
+ const head = window.document.querySelector('head');
69
+ linkHreflangs.forEach(item => {
70
+ const link = window.document.createElement('link');
71
+ link.setAttribute('rel', 'alternate');
72
+ link.setAttribute('hreflang', item.hreflang);
73
+ link.setAttribute('href', item.href);
74
+ head.appendChild(link);
75
+ });
76
+
77
+ window.document.documentElement.lang = htmlLang;
78
+
79
+
80
+ Object.defineProperty(navigator, 'language', {
81
+ value: navLang[0] || "",
82
+ writable: true
83
+ });
84
+
85
+ Object.defineProperty(navigator, 'languages', {
86
+ value: navLang,
87
+ writable: true
88
+ });
89
+ }
90
+
91
+ describe('getPreferredLanguage()', () => {
92
+
93
+ // 1) No document language, no navigator.languages => "No language information available."
94
+ it('should return "No language information available." when no current and no user preferences', () => {
95
+ setupDOM({htmlLang: '', linkHreflangs: [], navLang: []});
96
+
97
+ const result = getPreferredLanguage();
98
+ expect(result).to.have.property('message').that.not.empty;
99
+ });
100
+
101
+ // 2) Document lang is set, but no <link> tags => "No <link> tags with hreflang available."
102
+ it('should return "No <link> tags with hreflang available." when there are no link tags', () => {
103
+ setupDOM({htmlLang: 'en', linkHreflangs: []});
104
+ window.navigator.languages = [];
105
+
106
+ const result = getPreferredLanguage();
107
+ expect(result).to.have.property('current', 'en');
108
+ expect(result).to.have.property('message').that.equals('No <link> tags with hreflang available.');
109
+ });
110
+
111
+ // 3) Document lang 'en', no user preferences, but link tags exist => "None of the preferred languages are available."
112
+ it('should return "None of the preferred languages are available." when there are link tags but no matching user preferences', () => {
113
+ setupDOM({
114
+ htmlLang: 'en',
115
+ linkHreflangs: [
116
+ {hreflang: 'de', href: 'http://example.com/de'},
117
+ {hreflang: 'fr', href: 'http://example.com/fr'}
118
+ ],
119
+ navLang: []
120
+ });
121
+
122
+ const result = getPreferredLanguage();
123
+ expect(result).to.have.property('current', 'en');
124
+ expect(result).to.have.property('message').that.equals('No available languages match the user\'s preferences.');
125
+ expect(result.available).to.be.an('array').that.has.lengthOf(2);
126
+ });
127
+
128
+ // 4) Document lang 'en', user prefers ['en'], link tags with 'en' and 'de' => best match is 'en'
129
+ it('should return best match = "en" when user prefers en', () => {
130
+ setupDOM({
131
+ htmlLang: 'en',
132
+ linkHreflangs: [
133
+ {hreflang: 'en', href: 'http://example.com/en'},
134
+ {hreflang: 'de', href: 'http://example.com/de'}
135
+ ]
136
+ });
137
+ window.navigator.languages = ['en'];
138
+
139
+ const result = getPreferredLanguage();
140
+ expect(result).to.have.property('current', 'en');
141
+ console.log(JSON.stringify(result.preferred));
142
+ expect(result.preferred).to.have.property('full', "en");
143
+ expect(result.preferred).to.have.property('base', "en");
144
+ expect(result.preferred).to.have.property('label', "English");
145
+ expect(result.preferred).to.have.property('href', "http://example.com/en");
146
+ });
147
+
148
+ // 5) Document lang 'de-DE', user prefers ['de-DE'], link tags with 'de-DE' & 'en-US' => best match is 'de-DE'
149
+ it('should return best match = "de-DE" when user prefers de-DE', () => {
150
+ setupDOM({
151
+ htmlLang: 'de-DE',
152
+ linkHreflangs: [
153
+ {hreflang: 'de-DE', href: 'http://example.com/de-DE'},
154
+ {hreflang: 'en-US', href: 'http://example.com/en-US'}
155
+ ]
156
+ });
157
+ window.navigator.languages = ['de-DE'];
158
+
159
+ const result = getPreferredLanguage();
160
+ expect(result).to.have.property('current', 'de-DE');
161
+
162
+ expect(result.preferred).to.have.property('full', "de-DE");
163
+ expect(result.preferred).to.have.property('base', "de");
164
+ expect(result.preferred).to.have.property('label', "Deutsch (Deutschland)");
165
+ expect(result.preferred).to.have.property('href', "http://example.com/de-DE");
166
+
167
+ });
168
+
169
+
170
+ // 5x) Document lang 'de-DE', user prefers ['de-DE'], link tags with 'de-DE' & 'en-US' => best match is 'de-DE'
171
+ it('should return best match = "de-DE" when user prefers de-DE', () => {
172
+ setupDOM({
173
+ htmlLang: 'en',
174
+ linkHreflangs: [
175
+ {hreflang: 'de', href: 'http://example.com/de-DE'},
176
+ {hreflang: 'en', href: 'http://example.com/en-US'}
177
+ ]
178
+ });
179
+ window.navigator.languages = ['de-DE'];
180
+
181
+ const result = getPreferredLanguage();
182
+ expect(result).to.have.property('current', 'en');
183
+
184
+ expect(result.preferred).to.have.property('full', "de");
185
+ expect(result.preferred).to.have.property('base', "de");
186
+ expect(result.preferred).to.have.property('label', "Deutsch");
187
+ expect(result.preferred).to.have.property('href', "http://example.com/de-DE");
188
+
189
+ });
190
+
191
+ // 6) Document lang 'de-DE', user prefers ['en-US', 'en'], link tags with 'en-US' & 'en-GB' => best match = 'en-US'
192
+ it('should return best match = "en-US" for user preferences [en-US, en]', () => {
193
+ setupDOM({
194
+ htmlLang: 'de-DE',
195
+ linkHreflangs: [
196
+ {hreflang: 'en-US', href: 'http://example.com/en-US'},
197
+ {hreflang: 'en-GB', href: 'http://example.com/en-GB'}
198
+ ]
199
+ });
200
+ window.navigator.languages = ['en-US', 'en'];
201
+
202
+ const result = getPreferredLanguage();
203
+ expect(result).to.have.property('current', 'de-DE');
204
+ expect(result.preferred).to.have.property('full', "en-US");
205
+ expect(result.preferred).to.have.property('base', "en");
206
+ expect(result.preferred).to.have.property('label', "English (United States)");
207
+ expect(result.preferred).to.have.property('href', "http://example.com/en-US");
208
+
209
+ });
210
+
211
+ // 7) Document lang 'de-DE', user prefers ['de','en'], link tags with 'de-DE' & 'en-US' => best match = 'de-DE' (baseLang = 'de')
212
+ it('should return best match = "de-DE" when user preferences include its base language "de"', () => {
213
+ setupDOM({
214
+ htmlLang: 'de-DE',
215
+ linkHreflangs: [
216
+ {hreflang: 'de-DE', href: 'http://example.com/de-DE'},
217
+ {hreflang: 'en-US', href: 'http://example.com/en-US'}
218
+ ]
219
+ });
220
+ window.navigator.languages = ['de', 'en'];
221
+
222
+ const result = getPreferredLanguage();
223
+ expect(result).to.have.property('current', 'de-DE');
224
+
225
+ expect(result.preferred).to.have.property('full', "de-DE");
226
+ expect(result.preferred).to.have.property('base', "de");
227
+ expect(result.preferred).to.have.property('label', "Deutsch (Deutschland)");
228
+ expect(result.preferred).to.have.property('href', "http://example.com/de-DE");
229
+
230
+ });
231
+
232
+ // 8) Multiple link tags share the same weight => the first in the sorted array should be returned
233
+ it('should return the first item when multiple link tags have the same weight', () => {
234
+ setupDOM({
235
+ htmlLang: 'en',
236
+ linkHreflangs: [
237
+ {hreflang: 'fr', href: 'http://example.com/fr'},
238
+ {hreflang: 'es', href: 'http://example.com/es'}
239
+ ]
240
+ });
241
+ // Neither "fr" nor "es" is in the user preferences => both have weight = 1
242
+ window.navigator.languages = ['de'];
243
+
244
+ const result = getPreferredLanguage();
245
+ // Both 'fr' and 'es' have weight 1. After sorting, 'fr' appears first (as it was added first).
246
+ expect(result.available[0].fullLang).to.equal('fr');
247
+ expect(result.available[1].fullLang).to.equal('es');
248
+ expect(result.offerable).to.be.undefined;
249
+ expect(result).to.have.property('message', 'No available languages match the user\'s preferences.');
250
+ });
251
+
252
+ // 9) Check that bestURL is returned if a best match is found
253
+ it('should include bestURL when a best match is found', () => {
254
+ setupDOM({
255
+ htmlLang: 'fr',
256
+ linkHreflangs: [
257
+ {hreflang: 'fr-FR', href: 'http://example.com/fr-FR'},
258
+ {hreflang: 'de-DE', href: 'http://example.com/de-DE'}
259
+ ]
260
+ });
261
+ window.navigator.languages = ['fr-FR', 'de-DE'];
262
+
263
+ const result = getPreferredLanguage();
264
+
265
+ expect(result.preferred).to.have.property('full', "fr-FR");
266
+ expect(result.preferred).to.have.property('base', "fr");
267
+ expect(result.preferred).to.have.property('label', "Français (France)");
268
+ expect(result.preferred).to.have.property('href', "http://example.com/fr-FR");
269
+
270
+ });
271
+
272
+ // 10) Check presence of availableLanguages array, ensuring it has weight info
273
+ it('should return availableLanguages with weight for each link', () => {
274
+ setupDOM({
275
+ htmlLang: 'en-GB',
276
+ linkHreflangs: [
277
+ {hreflang: 'en-GB', href: 'http://example.com/en-GB'},
278
+ {hreflang: 'en-US', href: 'http://example.com/en-US'}
279
+ ]
280
+ });
281
+ window.navigator.languages = ['en-GB', 'en', 'en-US'];
282
+
283
+ const result = getPreferredLanguage();
284
+ expect(result.available).to.be.an('array').with.lengthOf(2);
285
+ result.available.forEach(item => {
286
+ expect(item).to.have.property('weight');
287
+ expect(item).to.have.property('fullLang');
288
+ expect(item).to.have.property('baseLang');
289
+ expect(item).to.have.property('href');
290
+ });
291
+ });
292
+
293
+ });
294
+
295
+ })