i18nextBrowserLanguageDetector.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450
  1. (function (global, factory) {
  2. typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
  3. typeof define === 'function' && define.amd ? define(factory) :
  4. (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.i18nextBrowserLanguageDetector = factory());
  5. })(this, (function () { 'use strict';
  6. const {
  7. slice,
  8. forEach
  9. } = [];
  10. function defaults(obj) {
  11. forEach.call(slice.call(arguments, 1), source => {
  12. if (source) {
  13. for (const prop in source) {
  14. if (obj[prop] === undefined) obj[prop] = source[prop];
  15. }
  16. }
  17. });
  18. return obj;
  19. }
  20. function hasXSS(input) {
  21. if (typeof input !== 'string') return false;
  22. // Common XSS attack patterns
  23. const xssPatterns = [/<\s*script.*?>/i, /<\s*\/\s*script\s*>/i, /<\s*img.*?on\w+\s*=/i, /<\s*\w+\s*on\w+\s*=.*?>/i, /javascript\s*:/i, /vbscript\s*:/i, /expression\s*\(/i, /eval\s*\(/i, /alert\s*\(/i, /document\.cookie/i, /document\.write\s*\(/i, /window\.location/i, /innerHTML/i];
  24. return xssPatterns.some(pattern => pattern.test(input));
  25. }
  26. // eslint-disable-next-line no-control-regex
  27. const fieldContentRegExp = /^[\u0009\u0020-\u007e\u0080-\u00ff]+$/;
  28. const serializeCookie = function (name, val) {
  29. let options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {
  30. path: '/'
  31. };
  32. const opt = options;
  33. const value = encodeURIComponent(val);
  34. let str = `${name}=${value}`;
  35. if (opt.maxAge > 0) {
  36. const maxAge = opt.maxAge - 0;
  37. if (Number.isNaN(maxAge)) throw new Error('maxAge should be a Number');
  38. str += `; Max-Age=${Math.floor(maxAge)}`;
  39. }
  40. if (opt.domain) {
  41. if (!fieldContentRegExp.test(opt.domain)) {
  42. throw new TypeError('option domain is invalid');
  43. }
  44. str += `; Domain=${opt.domain}`;
  45. }
  46. if (opt.path) {
  47. if (!fieldContentRegExp.test(opt.path)) {
  48. throw new TypeError('option path is invalid');
  49. }
  50. str += `; Path=${opt.path}`;
  51. }
  52. if (opt.expires) {
  53. if (typeof opt.expires.toUTCString !== 'function') {
  54. throw new TypeError('option expires is invalid');
  55. }
  56. str += `; Expires=${opt.expires.toUTCString()}`;
  57. }
  58. if (opt.httpOnly) str += '; HttpOnly';
  59. if (opt.secure) str += '; Secure';
  60. if (opt.sameSite) {
  61. const sameSite = typeof opt.sameSite === 'string' ? opt.sameSite.toLowerCase() : opt.sameSite;
  62. switch (sameSite) {
  63. case true:
  64. str += '; SameSite=Strict';
  65. break;
  66. case 'lax':
  67. str += '; SameSite=Lax';
  68. break;
  69. case 'strict':
  70. str += '; SameSite=Strict';
  71. break;
  72. case 'none':
  73. str += '; SameSite=None';
  74. break;
  75. default:
  76. throw new TypeError('option sameSite is invalid');
  77. }
  78. }
  79. if (opt.partitioned) str += '; Partitioned';
  80. return str;
  81. };
  82. const cookie = {
  83. create(name, value, minutes, domain) {
  84. let cookieOptions = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : {
  85. path: '/',
  86. sameSite: 'strict'
  87. };
  88. if (minutes) {
  89. cookieOptions.expires = new Date();
  90. cookieOptions.expires.setTime(cookieOptions.expires.getTime() + minutes * 60 * 1000);
  91. }
  92. if (domain) cookieOptions.domain = domain;
  93. document.cookie = serializeCookie(name, value, cookieOptions);
  94. },
  95. read(name) {
  96. const nameEQ = `${name}=`;
  97. const ca = document.cookie.split(';');
  98. for (let i = 0; i < ca.length; i++) {
  99. let c = ca[i];
  100. while (c.charAt(0) === ' ') c = c.substring(1, c.length);
  101. if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
  102. }
  103. return null;
  104. },
  105. remove(name, domain) {
  106. this.create(name, '', -1, domain);
  107. }
  108. };
  109. var cookie$1 = {
  110. name: 'cookie',
  111. // Deconstruct the options object and extract the lookupCookie property
  112. lookup(_ref) {
  113. let {
  114. lookupCookie
  115. } = _ref;
  116. if (lookupCookie && typeof document !== 'undefined') {
  117. return cookie.read(lookupCookie) || undefined;
  118. }
  119. return undefined;
  120. },
  121. // Deconstruct the options object and extract the lookupCookie, cookieMinutes, cookieDomain, and cookieOptions properties
  122. cacheUserLanguage(lng, _ref2) {
  123. let {
  124. lookupCookie,
  125. cookieMinutes,
  126. cookieDomain,
  127. cookieOptions
  128. } = _ref2;
  129. if (lookupCookie && typeof document !== 'undefined') {
  130. cookie.create(lookupCookie, lng, cookieMinutes, cookieDomain, cookieOptions);
  131. }
  132. }
  133. };
  134. var querystring = {
  135. name: 'querystring',
  136. // Deconstruct the options object and extract the lookupQuerystring property
  137. lookup(_ref) {
  138. let {
  139. lookupQuerystring
  140. } = _ref;
  141. let found;
  142. if (typeof window !== 'undefined') {
  143. let {
  144. search
  145. } = window.location;
  146. if (!window.location.search && window.location.hash?.indexOf('?') > -1) {
  147. search = window.location.hash.substring(window.location.hash.indexOf('?'));
  148. }
  149. const query = search.substring(1);
  150. const params = query.split('&');
  151. for (let i = 0; i < params.length; i++) {
  152. const pos = params[i].indexOf('=');
  153. if (pos > 0) {
  154. const key = params[i].substring(0, pos);
  155. if (key === lookupQuerystring) {
  156. found = params[i].substring(pos + 1);
  157. }
  158. }
  159. }
  160. }
  161. return found;
  162. }
  163. };
  164. var hash = {
  165. name: 'hash',
  166. // Deconstruct the options object and extract the lookupHash property and the lookupFromHashIndex property
  167. lookup(_ref) {
  168. let {
  169. lookupHash,
  170. lookupFromHashIndex
  171. } = _ref;
  172. let found;
  173. if (typeof window !== 'undefined') {
  174. const {
  175. hash
  176. } = window.location;
  177. if (hash && hash.length > 2) {
  178. const query = hash.substring(1);
  179. if (lookupHash) {
  180. const params = query.split('&');
  181. for (let i = 0; i < params.length; i++) {
  182. const pos = params[i].indexOf('=');
  183. if (pos > 0) {
  184. const key = params[i].substring(0, pos);
  185. if (key === lookupHash) {
  186. found = params[i].substring(pos + 1);
  187. }
  188. }
  189. }
  190. }
  191. if (found) return found;
  192. if (!found && lookupFromHashIndex > -1) {
  193. const language = hash.match(/\/([a-zA-Z-]*)/g);
  194. if (!Array.isArray(language)) return undefined;
  195. const index = typeof lookupFromHashIndex === 'number' ? lookupFromHashIndex : 0;
  196. return language[index]?.replace('/', '');
  197. }
  198. }
  199. }
  200. return found;
  201. }
  202. };
  203. let hasLocalStorageSupport = null;
  204. const localStorageAvailable = () => {
  205. if (hasLocalStorageSupport !== null) return hasLocalStorageSupport;
  206. try {
  207. hasLocalStorageSupport = typeof window !== 'undefined' && window.localStorage !== null;
  208. if (!hasLocalStorageSupport) {
  209. return false;
  210. }
  211. const testKey = 'i18next.translate.boo';
  212. window.localStorage.setItem(testKey, 'foo');
  213. window.localStorage.removeItem(testKey);
  214. } catch (e) {
  215. hasLocalStorageSupport = false;
  216. }
  217. return hasLocalStorageSupport;
  218. };
  219. var localStorage = {
  220. name: 'localStorage',
  221. // Deconstruct the options object and extract the lookupLocalStorage property
  222. lookup(_ref) {
  223. let {
  224. lookupLocalStorage
  225. } = _ref;
  226. if (lookupLocalStorage && localStorageAvailable()) {
  227. return window.localStorage.getItem(lookupLocalStorage) || undefined; // Undefined ensures type consistency with the previous version of this function
  228. }
  229. return undefined;
  230. },
  231. // Deconstruct the options object and extract the lookupLocalStorage property
  232. cacheUserLanguage(lng, _ref2) {
  233. let {
  234. lookupLocalStorage
  235. } = _ref2;
  236. if (lookupLocalStorage && localStorageAvailable()) {
  237. window.localStorage.setItem(lookupLocalStorage, lng);
  238. }
  239. }
  240. };
  241. let hasSessionStorageSupport = null;
  242. const sessionStorageAvailable = () => {
  243. if (hasSessionStorageSupport !== null) return hasSessionStorageSupport;
  244. try {
  245. hasSessionStorageSupport = typeof window !== 'undefined' && window.sessionStorage !== null;
  246. if (!hasSessionStorageSupport) {
  247. return false;
  248. }
  249. const testKey = 'i18next.translate.boo';
  250. window.sessionStorage.setItem(testKey, 'foo');
  251. window.sessionStorage.removeItem(testKey);
  252. } catch (e) {
  253. hasSessionStorageSupport = false;
  254. }
  255. return hasSessionStorageSupport;
  256. };
  257. var sessionStorage = {
  258. name: 'sessionStorage',
  259. lookup(_ref) {
  260. let {
  261. lookupSessionStorage
  262. } = _ref;
  263. if (lookupSessionStorage && sessionStorageAvailable()) {
  264. return window.sessionStorage.getItem(lookupSessionStorage) || undefined;
  265. }
  266. return undefined;
  267. },
  268. cacheUserLanguage(lng, _ref2) {
  269. let {
  270. lookupSessionStorage
  271. } = _ref2;
  272. if (lookupSessionStorage && sessionStorageAvailable()) {
  273. window.sessionStorage.setItem(lookupSessionStorage, lng);
  274. }
  275. }
  276. };
  277. var navigator$1 = {
  278. name: 'navigator',
  279. lookup(options) {
  280. const found = [];
  281. if (typeof navigator !== 'undefined') {
  282. const {
  283. languages,
  284. userLanguage,
  285. language
  286. } = navigator;
  287. if (languages) {
  288. // chrome only; not an array, so can't use .push.apply instead of iterating
  289. for (let i = 0; i < languages.length; i++) {
  290. found.push(languages[i]);
  291. }
  292. }
  293. if (userLanguage) {
  294. found.push(userLanguage);
  295. }
  296. if (language) {
  297. found.push(language);
  298. }
  299. }
  300. return found.length > 0 ? found : undefined;
  301. }
  302. };
  303. var htmlTag = {
  304. name: 'htmlTag',
  305. // Deconstruct the options object and extract the htmlTag property
  306. lookup(_ref) {
  307. let {
  308. htmlTag
  309. } = _ref;
  310. let found;
  311. const internalHtmlTag = htmlTag || (typeof document !== 'undefined' ? document.documentElement : null);
  312. if (internalHtmlTag && typeof internalHtmlTag.getAttribute === 'function') {
  313. found = internalHtmlTag.getAttribute('lang');
  314. }
  315. return found;
  316. }
  317. };
  318. var path = {
  319. name: 'path',
  320. // Deconstruct the options object and extract the lookupFromPathIndex property
  321. lookup(_ref) {
  322. let {
  323. lookupFromPathIndex
  324. } = _ref;
  325. if (typeof window === 'undefined') return undefined;
  326. const language = window.location.pathname.match(/\/([a-zA-Z-]*)/g);
  327. if (!Array.isArray(language)) return undefined;
  328. const index = typeof lookupFromPathIndex === 'number' ? lookupFromPathIndex : 0;
  329. return language[index]?.replace('/', '');
  330. }
  331. };
  332. var subdomain = {
  333. name: 'subdomain',
  334. lookup(_ref) {
  335. let {
  336. lookupFromSubdomainIndex
  337. } = _ref;
  338. // If given get the subdomain index else 1
  339. const internalLookupFromSubdomainIndex = typeof lookupFromSubdomainIndex === 'number' ? lookupFromSubdomainIndex + 1 : 1;
  340. // get all matches if window.location. is existing
  341. // first item of match is the match itself and the second is the first group match which should be the first subdomain match
  342. // is the hostname no public domain get the or option of localhost
  343. const language = typeof window !== 'undefined' && window.location?.hostname?.match(/^(\w{2,5})\.(([a-z0-9-]{1,63}\.[a-z]{2,6})|localhost)/i);
  344. // if there is no match (null) return undefined
  345. if (!language) return undefined;
  346. // return the given group match
  347. return language[internalLookupFromSubdomainIndex];
  348. }
  349. };
  350. // some environments, throws when accessing document.cookie
  351. let canCookies = false;
  352. try {
  353. // eslint-disable-next-line no-unused-expressions
  354. document.cookie;
  355. canCookies = true;
  356. // eslint-disable-next-line no-empty
  357. } catch (e) {}
  358. const order = ['querystring', 'cookie', 'localStorage', 'sessionStorage', 'navigator', 'htmlTag'];
  359. if (!canCookies) order.splice(1, 1);
  360. const getDefaults = () => ({
  361. order,
  362. lookupQuerystring: 'lng',
  363. lookupCookie: 'i18next',
  364. lookupLocalStorage: 'i18nextLng',
  365. lookupSessionStorage: 'i18nextLng',
  366. // cache user language
  367. caches: ['localStorage'],
  368. excludeCacheFor: ['cimode'],
  369. // cookieMinutes: 10,
  370. // cookieDomain: 'myDomain'
  371. convertDetectedLanguage: l => l
  372. });
  373. class Browser {
  374. constructor(services) {
  375. let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
  376. this.type = 'languageDetector';
  377. this.detectors = {};
  378. this.init(services, options);
  379. }
  380. init() {
  381. let services = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {
  382. languageUtils: {}
  383. };
  384. let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
  385. let i18nOptions = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
  386. this.services = services;
  387. this.options = defaults(options, this.options || {}, getDefaults());
  388. if (typeof this.options.convertDetectedLanguage === 'string' && this.options.convertDetectedLanguage.indexOf('15897') > -1) {
  389. this.options.convertDetectedLanguage = l => l.replace('-', '_');
  390. }
  391. // backwards compatibility
  392. if (this.options.lookupFromUrlIndex) this.options.lookupFromPathIndex = this.options.lookupFromUrlIndex;
  393. this.i18nOptions = i18nOptions;
  394. this.addDetector(cookie$1);
  395. this.addDetector(querystring);
  396. this.addDetector(localStorage);
  397. this.addDetector(sessionStorage);
  398. this.addDetector(navigator$1);
  399. this.addDetector(htmlTag);
  400. this.addDetector(path);
  401. this.addDetector(subdomain);
  402. this.addDetector(hash);
  403. }
  404. addDetector(detector) {
  405. this.detectors[detector.name] = detector;
  406. return this;
  407. }
  408. detect() {
  409. let detectionOrder = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.options.order;
  410. let detected = [];
  411. detectionOrder.forEach(detectorName => {
  412. if (this.detectors[detectorName]) {
  413. let lookup = this.detectors[detectorName].lookup(this.options);
  414. if (lookup && typeof lookup === 'string') lookup = [lookup];
  415. if (lookup) detected = detected.concat(lookup);
  416. }
  417. });
  418. detected = detected.filter(d => d !== undefined && d !== null && !hasXSS(d)).map(d => this.options.convertDetectedLanguage(d));
  419. if (this.services && this.services.languageUtils && this.services.languageUtils.getBestMatchFromCodes) return detected; // new i18next v19.5.0
  420. return detected.length > 0 ? detected[0] : null; // a little backward compatibility
  421. }
  422. cacheUserLanguage(lng) {
  423. let caches = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : this.options.caches;
  424. if (!caches) return;
  425. if (this.options.excludeCacheFor && this.options.excludeCacheFor.indexOf(lng) > -1) return;
  426. caches.forEach(cacheName => {
  427. if (this.detectors[cacheName]) this.detectors[cacheName].cacheUserLanguage(lng, this.options);
  428. });
  429. }
  430. }
  431. Browser.type = 'languageDetector';
  432. return Browser;
  433. }));