You may either normalize string and usestring.includes
// inspired by https://mcmap.net/q/667750/-filtering-a-list-of-strings-based-on-user-locale
/**
* Returns true if searchString appears as a substring of the result of converting first argument
* to a String, at one or more positions that are greater than or equal to position,
* if compared in the current or specified locale; otherwise, returns false.
* Options is considered to have { usage: 'search', sensitivity: 'base' } defaults
* @param {string} string search string
* @param {string} searchString search string
* @param {string|string[]=} locales A locale string or array of locale strings that contain one or more language or locale tags. If you include more than one locale string, list them in descending order of priority so that the first entry is the preferred locale. If you omit this parameter, the default locale of the JavaScript runtime is used. This parameter must conform to BCP 47 standards; see the Intl.Collator object for details.
* @param {Intl.CollatorOptions=} options An object that contains one or more properties that specify comparison options. see the Intl.Collator object for details.
* @param {number=} position If position is undefined, 0 is assumed, so as to search all of the String.
* @returns {boolean}
*/
function localeIncludes(string, searchString, locales, options, position = 0) {
const optionsN = { usage: 'search', sensitivity: 'base', ...options ?? {} };
const collator = new Intl.Collator(locales, optionsN);
const { sensitivity, ignorePunctuation } = collator.resolvedOptions();
function localeNormalize(string) {
// `localeCompare` MUST `ToString` its arguments
// We want to normalize out strings so `u'` does not include `u`
let stringN = String(string).normalize('NFC');
// If comparison is case-insensitive we want to normalize case
if (sensitivity === 'base' || sensitivity === 'accent')
stringN = stringN.toLocaleLowerCase(locales);
// then we try to remove accents (you may cache letters in a Map to make it faster)
return stringN.replaceAll(/./g, (letter) => {
// first check if you can remove the character completely
if (ignorePunctuation) {
if (collator.compare(letter, '') === 0) return '';
}
let normalizedLetter = letter.normalize('NFD').replace(/[\u0300-\u036f]/gi, '');
/*
* // If you want you may add some custom normalizers (per-language)
* const mapSv = new Map([ ['w', 'v'], ['ß', 'SS'] ])
* if (lang === 'sv' && mapSv.has(letter)) return mapSv.get(letter);
*/
return letter !== normalizedLetter && collator.compare(letter, normalizedLetter) === 0 ? normalizedLetter : letter;
});
}
return localeNormalize(string).includes(localeNormalize(searchString));
}
or try to find a matching substring
/**
* Returns true if searchString appears as a substring of the result of converting first argument
* to a String, at one or more positions that are greater than or equal to position,
* if compared in the current or specified locale; otherwise, returns false.
* Collators with `numeric` and `ignorePunctuation` options are not supported.
* @param {string} string search string
* @param {string} searchString search string
* @param {string|string[]=} locales A locale string or array of locale strings that contain one or more language or locale tags. If you include more than one locale string, list them in descending order of priority so that the first entry is the preferred locale. If you omit this parameter, the default locale of the JavaScript runtime is used. This parameter must conform to BCP 47 standards; see the Intl.Collator object for details.
* @param {Intl.CollatorOptions=} options An object that contains one or more properties that specify comparison options. see the Intl.Collator object for details.
* @param {number=} position If position is undefined, 0 is assumed, so as to search all of the String.
* @returns {boolean}
*/
function localeIncludes(string, searchString, locales, options, position = 0) {
// `localeCompare` uses `Intl.Collator.compare` under the hood
// `localeCompare` casts `ToString` over both arguments
// We don't want "á" to contain "a", so we should normalize the strings first.
// `Intl.Collator` uses Canonical Equivalence according to the Unicode Standard, so normalization won't change the order
const stringN = String(string).normalize();
const searchStringN = String(searchString).normalize();
const collator = new Intl.Collator(locales, options);
/*
* // if you can have strings of different length (like with `ignorePunctuation`), you'll have to check every substring
* for (let i = 0; i < string.length; i++) {
* for (let j = i; j < string.length; j++) {
* // WARNING, THIS IS $ O(n^2) $
* let substring = string.substring(i, i + searchString.length);
* if (collator.compare(substring, searchString) === 0) return i;
* }
* }
*/
for (let i = position; i <= stringN.length - searchStringN.length; i++) {
// non-numeric non-ignorePunctuation `collator` expected
const substring = stringN.substring(i, i + searchStringN.length);
if (collator.compare(substring, searchStringN) === 0)
return true;
}
return false;
}
Or you can use a hack using window.find
which works exacly as if you search on the page with Ctrl-F
function iframeIncludes(string, searchString, locale) {
const iframe = document.createElement('iframe');
iframe.style = 'position: fixed; top: 0; left: 0;';
document.body.append(iframe);
const iframeDoc = f.contentDocument;
iframeDoc.open();
// you MUST use <pre> otherwise it doesn't work
iframeDoc.write(`
<html lang="${locale}">
<body>
<pre></pre>
</body>
</html>
`);
iframeDoc.close();
const pre = iframeDoc.querySelector('pre');
pre.innerText = string;
const result = iframe.contentWindow.find(searchString);
iframe.remove();
return result;
}