diff --git a/modules/core/LocalizationSystem.js b/modules/core/LocalizationSystem.js index 563b981e8..28333d57c 100644 --- a/modules/core/LocalizationSystem.js +++ b/modules/core/LocalizationSystem.js @@ -208,8 +208,8 @@ export class LocalizationSystem extends AbstractSystem { // 4. English (always fallback) const requestedLocales = (this._preferredLocaleCodes || []) .concat(urlLocaleCodes) - .concat(utilDetect().browserLocales) // Locales preferred by the browser in priority order. - .concat(['en']); // Fallback to English since it's the only guaranteed complete language + .concat(utilDetect().locales) // Locales preferred by the browser in priority order. + .concat(['en']); // Fallback to English since it's the only guaranteed complete language this._currLocaleCodes = this._getSupportedLocales(requestedLocales); this._currLocaleCode = this._currLocaleCodes[0]; // First is highest priority locale; the rest are fallbacks diff --git a/modules/util/detect.js b/modules/util/detect.js index e4851a35b..6f4479816 100644 --- a/modules/util/detect.js +++ b/modules/util/detect.js @@ -1,9 +1,26 @@ -let _detected; - +let _cached; + +/** + * `utilDetect` detects things from the user's browser. + * It returns an object with the following: + * { + * support: true, // Is Rapid supported? (basically - not Internet Explorer) + * browser: "Chrome", // e.g. 'Edge','msie','Opera','Chrome','Safari','Firefox' + * version: "133.0", // reported browser version + * languages: ['en-US'], // Array sourced from `navigator.languages` + * host: "http://127.0.0.1:8080/", + * os: "mac", + * platform: "Macintosh", + * prefersColorScheme: 'light', // 'light' or 'dark' + * prefersContrast: null, // 'more', 'less', or `null` + * prefersReducedMotion: false, // `true` or `false` + * prefersReducedTransparency: false // `true` or `false` + * } + */ export function utilDetect(refresh) { - if (_detected && !refresh) return _detected; - _detected = {}; + if (_cached && !refresh) return _cached; + _cached = {}; const ua = navigator.userAgent; let m = null; @@ -11,84 +28,63 @@ export function utilDetect(refresh) { /* Browser */ m = ua.match(/(edg)\/?\s*(\.?\d+(\.\d+)*)/i); // Edge if (m !== null) { - _detected.browser = 'Edge'; - _detected.version = m[2]; + _cached.browser = 'Edge'; + _cached.version = m[2]; } - if (!_detected.browser) { + if (!_cached.browser) { m = ua.match(/Trident\/.*rv:([0-9]{1,}[\.0-9]{0,})/i); // IE11 if (m !== null) { - _detected.browser = 'msie'; - _detected.version = m[1]; + _cached.browser = 'msie'; + _cached.version = m[1]; } } - if (!_detected.browser) { + if (!_cached.browser) { m = ua.match(/(opr)\/?\s*(\.?\d+(\.\d+)*)/i); // Opera 15+ if (m !== null) { - _detected.browser = 'Opera'; - _detected.version = m[2]; + _cached.browser = 'Opera'; + _cached.version = m[2]; } } - if (!_detected.browser) { + if (!_cached.browser) { m = ua.match(/(opera|chrome|safari|firefox|msie)\/?\s*(\.?\d+(\.\d+)*)/i); if (m !== null) { - _detected.browser = m[1]; - _detected.version = m[2]; + _cached.browser = m[1]; + _cached.version = m[2]; m = ua.match(/version\/([\.\d]+)/i); - if (m !== null) _detected.version = m[1]; + if (m !== null) _cached.version = m[1]; } } - if (!_detected.browser) { - _detected.browser = navigator.appName; - _detected.version = navigator.appVersion; + if (!_cached.browser) { + _cached.browser = navigator.appName; + _cached.version = navigator.appVersion; } - // keep major.minor version only.. - _detected.version = _detected.version.split(/\W/).slice(0,2).join('.'); - - // detect other browser capabilities - // Legacy Opera has incomplete svg style support. See #715 - _detected.opera = (_detected.browser.toLowerCase() === 'opera' && parseFloat(_detected.version) < 15 ); + // Keep major.minor version only.. + _cached.version = _cached.version.split(/\W/).slice(0,2).join('.'); - if (_detected.browser.toLowerCase() === 'msie') { - _detected.support = false; + if (_cached.browser.toLowerCase() === 'msie') { + _cached.support = false; } else { - _detected.support = true; + _cached.support = true; } - _detected.filedrop = (window.FileReader && 'ondrop' in window); - _detected.download = !(_detected.ie || _detected.browser.toLowerCase() === 'edge'); - _detected.cssfilters = !(_detected.ie || _detected.browser.toLowerCase() === 'edge'); - - /* Platform */ if (/Win/.test(ua)) { - _detected.os = 'win'; - _detected.platform = 'Windows'; + _cached.os = 'win'; + _cached.platform = 'Windows'; } else if (/Mac/.test(ua)) { - _detected.os = 'mac'; - _detected.platform = 'Macintosh'; + _cached.os = 'mac'; + _cached.platform = 'Macintosh'; } else if (/X11/.test(ua) || /Linux/.test(ua)) { - _detected.os = 'linux'; - _detected.platform = 'Linux'; + _cached.os = 'linux'; + _cached.platform = 'Linux'; } else { - _detected.os = 'win'; - _detected.platform = 'Unknown'; + _cached.os = 'win'; + _cached.platform = 'Unknown'; } - /* Locale */ - // An array of locales requested by the browser in priority order. - _detected.browserLocales = Array.from(new Set( // remove duplicates - [navigator.language] - .concat(navigator.languages || []) - .concat([ - // old property for backwards compatibility - navigator.userLanguage - ]) - // remove any undefined values - .filter(Boolean) - )); - + _cached.locales = navigator.languages.slice(); // shallow copy /* Host */ const loc = window.top.location; @@ -97,8 +93,13 @@ export function utilDetect(refresh) { origin = loc.protocol + '//' + loc.hostname + (loc.port ? ':' + loc.port: ''); } - _detected.host = origin + loc.pathname; + _cached.host = origin + loc.pathname; + _cached.prefersColorScheme = window.matchMedia?.('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; + _cached.prefersContrast = window.matchMedia?.('(prefers-contrast: more)').matches ? 'more' + : window.matchMedia?.('(prefers-contrast: less)').matches ? 'less' : null; + _cached.prefersReducedMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches; + _cached.prefersReducedTransparency = window.matchMedia?.('(prefers-reduced-transparency: reduce)').matches; - return _detected; + return _cached; } diff --git a/test/unit/util/detect.test.js b/test/unit/util/detect.test.js index 5290c4fce..24f478aa3 100644 --- a/test/unit/util/detect.test.js +++ b/test/unit/util/detect.test.js @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, it } from 'node:test'; +import { before, after, beforeEach, describe, it } from 'node:test'; import { strict as assert } from 'node:assert'; import * as Rapid from '../../../modules/headless.js'; @@ -15,45 +15,45 @@ if (!global.window) { // mock window for Node } describe('utilDetect', () => { - let navigator; - let originalNavigator = global.navigator; + let origNavigator; + + before(() => { + origNavigator = global.navigator; + }); + + after(() => { + global.navigator = origNavigator; // restore original + }); + beforeEach(() => { - // Create a mock navigator object with a languages and platform property - navigator = { - languages: ['en-US'], - platform: 'Windows' + const mock = { + languages: ['en-US', 'en'], + platform: 'MacIntel', + userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36' }; - // Set the mock navigator object as the global navigator object - global.navigator = navigator; - }); - afterEach(() => { - // Reset the global navigator object to its original value - global.navigator = originalNavigator; + // Copy the original navigator, so we can safely change things. + global.navigator = Object.assign(origNavigator || mock); }); it('should detect the browser and version', () => { const ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3'; - global.navigator = { userAgent: ua }; - const detected = Rapid.utilDetect(); + global.navigator.userAgent = ua; + const detected = Rapid.utilDetect(true); assert.strictEqual(detected.browser, 'Chrome'); assert.strictEqual(detected.version, '58.0'); }); - it('should detect the platform', () => { + it('should detect the os and platform', () => { const ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3'; - global.navigator = { userAgent: ua }; - const detected = Rapid.utilDetect(); + global.navigator.userAgent = ua; + const detected = Rapid.utilDetect(true); assert.strictEqual(detected.os, 'win'); assert.strictEqual(detected.platform, 'Windows'); }); it('should detect the locale', () => { - const detected = Rapid.utilDetect(); - assert.ok(!detected.browserLocales.includes('en-US')); - }); - - it('should detect the platform', () => { - const detected = Rapid.utilDetect(); - assert.strictEqual(detected.platform, 'Windows'); + global.navigator.languages = ['es']; + const detected = Rapid.utilDetect(true); + assert.ok(detected.locales.includes('es')); }); -}); \ No newline at end of file +});