From a2d01ff8dab8223b0c27480a4202960c4fec5fae Mon Sep 17 00:00:00 2001 From: welpo Date: Thu, 15 Feb 2024 00:54:36 +0100 Subject: [PATCH] pluralize search result strings --- static/js/searchElasticlunr.js | 85 ++++++++++++++++++++++++---- static/js/searchElasticlunr.min.js | 2 +- templates/partials/search_modal.html | 16 +++--- 3 files changed, 82 insertions(+), 21 deletions(-) diff --git a/static/js/searchElasticlunr.js b/static/js/searchElasticlunr.js index 481cd39af..e6c8aee13 100644 --- a/static/js/searchElasticlunr.js +++ b/static/js/searchElasticlunr.js @@ -2575,13 +2575,28 @@ window.onload = function () { return; } - const results = document.getElementById('results'); + const lang = document.documentElement.lang; const searchInput = document.getElementById('searchInput'); const searchModal = document.getElementById('searchModal'); const searchButton = document.getElementById('search-button'); - const nResultsSpan = document.getElementById('n-results'); const clearSearchButton = document.getElementById('clear-search'); const resultsContainer = document.getElementById('results-container'); + const results = document.getElementById('results'); + // Get all spans holding the translated strings, even if they are only used on one language. + const zeroResultsSpan = document.getElementById('zero_results'); + const oneResultsSpan = document.getElementById('one_results'); + const twoResultsSpan = document.getElementById('two_results'); + const fewResultsSpan = document.getElementById('few_results'); + const manyResultsSpan = document.getElementById('many_results'); + + // Static mapping of keys to spans. + const resultSpans = { + zero_results: zeroResultsSpan, + one_results: oneResultsSpan, + two_results: twoResultsSpan, + few_results: fewResultsSpan, + many_results: manyResultsSpan, + }; // Replace $SHORTCUT in search icon title with actual OS-specific shortcut. function getShortcut() { @@ -2658,7 +2673,6 @@ window.onload = function () { function clearSearch() { searchInput.value = ''; results.innerHTML = ''; - nResultsSpan.textContent = '0'; resultsContainer.style.display = 'none'; searchInput.removeAttribute('aria-activedescendant'); } @@ -3006,19 +3020,66 @@ window.onload = function () { ); function updateResultText(count) { - const nResultsSpan = document.getElementById('n-results'); - nResultsSpan.textContent = count.toString(); + // Determine the correct pluralization key based on count and language. + const pluralizationKey = getPluralizationKey(count, lang); + + // Hide all result text spans. + Object.values(resultSpans).forEach((span) => { + if (span) span.style.display = 'none'; + }); + + // Show the relevant result text span, replacing $NUMBER with the actual count. + const activeSpan = resultSpans[pluralizationKey]; + if (activeSpan) { + activeSpan.style.display = 'inline'; + activeSpan.textContent = activeSpan.textContent.replace( + '$NUMBER', + count.toString() + ); + } + } - const singular = document.getElementById('result-text-singular'); - const plural = document.getElementById('result-text-plural'); + function getPluralizationKey(count, lang) { + let key = ''; + const slavicLangs = ['uk', 'be', 'bs', 'hr', 'ru', 'sr']; - if (count === 1) { - singular.style.display = 'inline'; - plural.style.display = 'none'; + // Common cases: zero, one. + if (count === 0) { + key = 'zero_results'; + } else if (count === 1) { + key = 'one_results'; } else { - singular.style.display = 'none'; - plural.style.display = 'inline'; + // Arabic. + if (lang === 'ar') { + let modulo = count % 100; + if (count === 2) { + key = 'two_results'; + } else if (modulo >= 3 && modulo <= 10) { + key = 'few_results'; + } else { + key = 'many_results'; + } + } else if (slavicLangs.includes(lang)) { + // Slavic languages. + let modulo10 = count % 10; + let modulo100 = count % 100; + if (modulo10 === 1 && modulo100 !== 11) { + key = 'one_results'; + } else if ( + modulo10 >= 2 && + modulo10 <= 4 && + !(modulo100 >= 12 && modulo100 <= 14) + ) { + key = 'few_results'; + } else { + key = 'many_results'; + } + } else { + key = 'many_results'; // Default plural. + } } + + return key; } function setupTouchEvents() { diff --git a/static/js/searchElasticlunr.min.js b/static/js/searchElasticlunr.min.js index abd7cb1e5..b6b11a9eb 100644 --- a/static/js/searchElasticlunr.min.js +++ b/static/js/searchElasticlunr.min.js @@ -1 +1 @@ -!function(){const a=function(e){var t=new a.Index;return t.pipeline.add(a.trimmer,a.stopWordFilter,a.stemmer),e&&e.call(t,t),t};var t;a.version="0.9.5",(lunr=a).utils={},a.utils.warn=(t=this,function(e){t.console&&console.warn&&console.warn(e)}),a.utils.toString=function(e){return null==e?"":e.toString()},a.EventEmitter=function(){this.events={}},a.EventEmitter.prototype.addListener=function(){const e=Array.prototype.slice.call(arguments),t=e.pop(),n=e;if("function"!=typeof t)throw new TypeError("last argument must be a function");n.forEach(function(e){this.hasHandler(e)||(this.events[e]=[]),this.events[e].push(t)},this)},a.EventEmitter.prototype.removeListener=function(e,t){this.hasHandler(e)&&-1!==(t=this.events[e].indexOf(t))&&(this.events[e].splice(t,1),0===this.events[e].length)&&delete this.events[e]},a.EventEmitter.prototype.emit=function(e){if(this.hasHandler(e)){const t=Array.prototype.slice.call(arguments,1);this.events[e].forEach(function(e){e.apply(void 0,t)},this)}},a.EventEmitter.prototype.hasHandler=function(e){return e in this.events},a.tokenizer=function(n){if(!arguments.length||null==n)return[];if(Array.isArray(n)){let e=n.filter(function(e){return null!=e}),t=(e=e.map(function(e){return a.utils.toString(e).toLowerCase()}),[]);return e.forEach(function(e){e=e.split(a.tokenizer.seperator),t=t.concat(e)},this),t}return n.toString().trim().toLowerCase().split(a.tokenizer.seperator)},a.tokenizer.defaultSeperator=/[\s-]+/,a.tokenizer.seperator=a.tokenizer.defaultSeperator,a.tokenizer.setSeperator=function(e){null!=e&&"object"==typeof e&&(a.tokenizer.seperator=e)},a.tokenizer.resetSeperator=function(){a.tokenizer.seperator=a.tokenizer.defaultSeperator},a.tokenizer.getSeperator=function(){return a.tokenizer.seperator},a.Pipeline=function(){this._queue=[]},a.Pipeline.registeredFunctions={},a.Pipeline.registerFunction=function(e,t){t in a.Pipeline.registeredFunctions&&a.utils.warn("Overwriting existing registered function: "+t),e.label=t,a.Pipeline.registeredFunctions[t]=e},a.Pipeline.getRegisteredFunction=function(e){return e in a.Pipeline.registeredFunctions!=1?null:a.Pipeline.registeredFunctions[e]},a.Pipeline.warnIfFunctionNotRegistered=function(e){e.label&&e.label in this.registeredFunctions||a.utils.warn("Function is not registered with pipeline. This may cause problems when serialising the index.\n",e)},a.Pipeline.load=function(e){const n=new a.Pipeline;return e.forEach(function(e){var t=a.Pipeline.getRegisteredFunction(e);if(!t)throw new Error("Cannot load un-registered function: "+e);n.add(t)}),n},a.Pipeline.prototype.add=function(){Array.prototype.slice.call(arguments).forEach(function(e){a.Pipeline.warnIfFunctionNotRegistered(e),this._queue.push(e)},this)},a.Pipeline.prototype.after=function(e,t){if(a.Pipeline.warnIfFunctionNotRegistered(t),-1===(e=this._queue.indexOf(e)))throw new Error("Cannot find existingFn");this._queue.splice(e+1,0,t)},a.Pipeline.prototype.before=function(e,t){if(a.Pipeline.warnIfFunctionNotRegistered(t),-1===(e=this._queue.indexOf(e)))throw new Error("Cannot find existingFn");this._queue.splice(e,0,t)},a.Pipeline.prototype.remove=function(e){-1!==(e=this._queue.indexOf(e))&&this._queue.splice(e,1)},a.Pipeline.prototype.run=function(o){var e=[],t=o.length,i=this._queue.length;for(let n=0;ne&&(n=i),o=n-t,i=t+Math.floor(o/2),r=this.elements[i]}return r===e?i:-1},lunr.SortedSet.prototype.locationFor=function(e){let t=0,n=this.elements.length,o=n-t,i=t+Math.floor(o/2),r=this.elements[i];for(;1e&&(n=i),o=n-t,i=t+Math.floor(o/2),r=this.elements[i];return r>e?i:ri-1||o>r-1);)s[n]!==l[o]?s[n]l[o]&&o++:(t.add(s[n]),n++,o++);return t},lunr.SortedSet.prototype.clone=function(){var e=new lunr.SortedSet;return e.elements=this.toArray(),e.length=e.elements.length,e},lunr.SortedSet.prototype.union=function(e){let t,n,o;n=this.length>=e.length?(t=this,e):(t=e,this),o=t.clone();for(let e=0,t=n.toArray();e{let t=n.getAttribute(e);t&&(t=t.replace("$SHORTCUT",v),n.setAttribute(e,t))})}s.addEventListener("keydown",function(e){"Enter"!==e.key&&" "!==e.key||s.click()}),h.addEventListener("click",o),h.addEventListener("touchend",o),document.addEventListener("keydown",function(e){"Escape"===e.key&&u()}),y.addEventListener("click",function(){t(),f.focus()}),y.addEventListener("keydown",function(e){"Enter"!==e.key&&" "!==e.key||(t(),f.focus(),e.preventDefault())}),s.addEventListener("mouseover",i),s.addEventListener("click",l),s.addEventListener("touchstart",l);let r=null;function l(){e=document.activeElement,i(),h.style.display="block",f.focus()}function u(){h.style.display="none",t(),e&&document.body.contains(e)&&e.focus()}function c(e){var t;"true"!==e.getAttribute("aria-selected")&&([t=null]=[e],d.querySelectorAll("#results > div").forEach(e=>{e!==t&&e.setAttribute("aria-selected","false")}),e.setAttribute("aria-selected","true")),f.setAttribute("aria-activedescendant",e.id)}function t(){f.value="",d.innerHTML="",p.textContent="0",m.style.display="none",f.removeAttribute("aria-activedescendant")}function o(e){e.target===h&&u(),e.stopPropagation()}function i(){if(!r)if(window.searchIndex)r=Promise.resolve(elasticlunr.Index.load(window.searchIndex));else{var t=document.documentElement.getAttribute("lang").substring(0,2);let e=document.querySelector("meta[name='base']").getAttribute("content");e.endsWith("/")&&(e=e.slice(0,-1)),r=fetch(e+"/search_index."+t+".json").then(e=>e.json()).then(e=>elasticlunr.Index.load(e))}}function g(e){return e=parseInt(e,16).toString(2),[0,1,2,3,4][Math.ceil(e.length/8)]}function a(){c(this)}f.addEventListener("input",async function(){const s=this.value.trim(),e=await r;d.innerHTML="",m.style.display=0",n.querySelector("a")),i=n.querySelector("span:first-child"),r=n.querySelector("span:nth-child(2)"),i=(i.textContent=t.doc.title||t.doc.path||t.doc.id,t.doc.body?function(n,e){const o=150,i=e.map(function(e){return elasticlunr.stemmer(e.toLowerCase())});let r=0;var s=[],e=n.toLowerCase().split(". ");for(const n of e){const e=n.split(/[\s\n]/);let t=!0;for(const n of e){if(0o?n.substring(0,o)+"…":n;var t=[];let l=0;for(var u=0;u"),e[2]+e[0].length);if(!h.test(e[0])&&12<=e[0].length){const i=function(t){let n="",o=!1,i=0,r=0,s=0;for(let e=0;e"),f=o}d.push("…");var p=e=d.join("");return e.replace(/<[^>]+>/g,"").length>o?e.substring(0,o)+"…":p}(t.doc.body,s.split(/\s+/)):t.doc.description||"");r.innerHTML=i;let e=t.ref;t.doc.body&&(e+="#:~:text="+encodeURIComponent(s)),o.href=e,d.appendChild(n)}}),f.setAttribute("aria-expanded",0 div").forEach(e=>{e.removeEventListener("touchstart",a),e.addEventListener("touchstart",a)})},!0),document.addEventListener("keydown",function(t){var e=navigator.userAgent.toLowerCase().includes("mac")?t.metaKey:t.ctrlKey;if("k"===t.key&&e)t.preventDefault(),("block"===h.style.display?u:l)();else if(e=document.activeElement,"Tab"!==t.key||e!==f&&e!==y){if(0!==(r=d.querySelectorAll("#results > div")).length){var n,o,i=Array.from(r),r=d.querySelector('[aria-selected="true"]'),s=i.indexOf(r);if(["ArrowUp","ArrowDown","Home","End","PageUp","PageDown"].includes(t.key)){t.preventDefault();let e=s;switch(t.key){case"ArrowUp":e=Math.max(s-1,0);break;case"ArrowDown":e=Math.min(s+1,i.length-1);break;case"Home":e=0;break;case"End":e=i.length-1;break;case"PageUp":e=Math.max(s-3,0);break;case"PageDown":e=Math.min(s+3,i.length-1)}e!==s&&(c((o=i)[n=e]),o[n].scrollIntoView({block:"nearest",inline:"start"}))}if("Enter"===t.key&&r){t.preventDefault(),t.stopImmediatePropagation();const d=r.querySelector("a");d&&(window.location.href=d.getAttribute("href")),u()}}}else t.preventDefault(),(e===f?y:f).focus()})}}; +!function(){function p(e){var t=new p.Index;return t.pipeline.add(p.trimmer,p.stopWordFilter,p.stemmer),e&&e.call(t,t),t}var t;p.version="0.9.5",(lunr=p).utils={},p.utils.warn=(t=this,function(e){t.console&&console.warn&&console.warn(e)}),p.utils.toString=function(e){return null==e?"":e.toString()},(p.EventEmitter=function(){this.events={}}).prototype.addListener=function(){var e=Array.prototype.slice.call(arguments);const t=e.pop();if("function"!=typeof t)throw new TypeError("last argument must be a function");e.forEach(function(e){this.hasHandler(e)||(this.events[e]=[]),this.events[e].push(t)},this)},p.EventEmitter.prototype.removeListener=function(e,t){this.hasHandler(e)&&-1!==(t=this.events[e].indexOf(t))&&(this.events[e].splice(t,1),0===this.events[e].length)&&delete this.events[e]},p.EventEmitter.prototype.emit=function(e){if(this.hasHandler(e)){const t=Array.prototype.slice.call(arguments,1);this.events[e].forEach(function(e){e.apply(void 0,t)},this)}},p.EventEmitter.prototype.hasHandler=function(e){return e in this.events},(p.tokenizer=function(n){if(!arguments.length||null==n)return[];if(Array.isArray(n)){let e=n.filter(function(e){return null!=e}),t=(e=e.map(function(e){return p.utils.toString(e).toLowerCase()}),[]);return e.forEach(function(e){e=e.split(p.tokenizer.seperator),t=t.concat(e)},this),t}return n.toString().trim().toLowerCase().split(p.tokenizer.seperator)}).defaultSeperator=/[\s-]+/,p.tokenizer.seperator=p.tokenizer.defaultSeperator,p.tokenizer.setSeperator=function(e){null!=e&&"object"==typeof e&&(p.tokenizer.seperator=e)},p.tokenizer.resetSeperator=function(){p.tokenizer.seperator=p.tokenizer.defaultSeperator},p.tokenizer.getSeperator=function(){return p.tokenizer.seperator},(p.Pipeline=function(){this._queue=[]}).registeredFunctions={},p.Pipeline.registerFunction=function(e,t){t in p.Pipeline.registeredFunctions&&p.utils.warn("Overwriting existing registered function: "+t),e.label=t,p.Pipeline.registeredFunctions[t]=e},p.Pipeline.getRegisteredFunction=function(e){return e in p.Pipeline.registeredFunctions!=1?null:p.Pipeline.registeredFunctions[e]},p.Pipeline.warnIfFunctionNotRegistered=function(e){e.label&&e.label in this.registeredFunctions||p.utils.warn("Function is not registered with pipeline. This may cause problems when serialising the index.\n",e)},p.Pipeline.load=function(e){const n=new p.Pipeline;return e.forEach(function(e){var t=p.Pipeline.getRegisteredFunction(e);if(!t)throw new Error("Cannot load un-registered function: "+e);n.add(t)}),n},p.Pipeline.prototype.add=function(){Array.prototype.slice.call(arguments).forEach(function(e){p.Pipeline.warnIfFunctionNotRegistered(e),this._queue.push(e)},this)},p.Pipeline.prototype.after=function(e,t){if(p.Pipeline.warnIfFunctionNotRegistered(t),-1===(e=this._queue.indexOf(e)))throw new Error("Cannot find existingFn");this._queue.splice(e+1,0,t)},p.Pipeline.prototype.before=function(e,t){if(p.Pipeline.warnIfFunctionNotRegistered(t),-1===(e=this._queue.indexOf(e)))throw new Error("Cannot find existingFn");this._queue.splice(e,0,t)},p.Pipeline.prototype.remove=function(e){-1!==(e=this._queue.indexOf(e))&&this._queue.splice(e,1)},p.Pipeline.prototype.run=function(o){var e=[],t=o.length,i=this._queue.length;for(let n=0;ne&&(n=i),o=n-t,i=t+Math.floor(o/2),r=this.elements[i]}return r===e?i:-1},lunr.SortedSet.prototype.locationFor=function(e){let t=0,n=this.elements.length,o=n-t,i=t+Math.floor(o/2),r=this.elements[i];for(;1e&&(n=i),o=n-t,i=t+Math.floor(o/2),r=this.elements[i];return r>e?i:ri-1||o>r-1);)s[n]===l[o]?(t.add(s[n]),n++,o++):s[n]l[o]&&o++;return t},lunr.SortedSet.prototype.clone=function(){var e=new lunr.SortedSet;return e.elements=this.toArray(),e.length=e.elements.length,e},lunr.SortedSet.prototype.union=function(e){let t,n,o;n=this.length>=e.length?(t=this,e):(t=e,this),o=t.clone();for(let e=0,t=n.toArray();e{let t=n.getAttribute(e);t&&(t=t.replace("$SHORTCUT",w),n.setAttribute(e,t))})}s.addEventListener("keydown",function(e){"Enter"!==e.key&&" "!==e.key||s.click()});let e,r=(f.addEventListener("click",o),f.addEventListener("touchend",o),document.addEventListener("keydown",function(e){"Escape"===e.key&&u()}),p.addEventListener("click",function(){t(),h.focus()}),p.addEventListener("keydown",function(e){"Enter"!==e.key&&" "!==e.key||(t(),h.focus(),e.preventDefault())}),s.addEventListener("mouseover",i),s.addEventListener("click",l),s.addEventListener("touchstart",l),null);function l(){e=document.activeElement,i(),f.style.display="block",h.focus()}function u(){f.style.display="none",t(),e&&document.body.contains(e)&&e.focus()}function a(e){var t;"true"!==e.getAttribute("aria-selected")&&([t=null]=[e],m.querySelectorAll("#results > div").forEach(e=>{e!==t&&e.setAttribute("aria-selected","false")}),e.setAttribute("aria-selected","true")),h.setAttribute("aria-activedescendant",e.id)}function t(){h.value="",m.innerHTML="",g.style.display="none",h.removeAttribute("aria-activedescendant")}function o(e){e.target===f&&u(),e.stopPropagation()}function i(){if(!r)if(window.searchIndex)r=Promise.resolve(elasticlunr.Index.load(window.searchIndex));else{var t=document.documentElement.getAttribute("lang").substring(0,2);let e=document.querySelector("meta[name='base']").getAttribute("content");e.endsWith("/")&&(e=e.slice(0,-1)),r=fetch(e+"/search_index."+t+".json").then(e=>e.json()).then(e=>elasticlunr.Index.load(e))}}function y(e){return e=parseInt(e,16).toString(2),[0,1,2,3,4][Math.ceil(e.length/8)]}function c(){a(this)}h.addEventListener("input",async function(){const s=this.value.trim();var e=await r;m.innerHTML="",g.style.display=0{e&&(e.style.display="none")}),(i=v[i])&&(i.style.display="inline",i.textContent=i.textContent.replace("$NUMBER",n.toString()));let l=0;e.forEach(function(t){if(t.doc.title||t.doc.path||t.doc.id){var n=document.createElement("div"),o=(n.setAttribute("role","option"),n.id="result-"+l++,n.innerHTML="",n.querySelector("a")),i=n.querySelector("span:first-child"),r=n.querySelector("span:nth-child(2)");i.textContent=t.doc.title||t.doc.path||t.doc.id,i=t.doc.body?function(n,e){var o=e.map(function(e){return elasticlunr.stemmer(e.toLowerCase())});let i=0;var r=[];for(const e of n.toLowerCase().split(". ")){let t=!0;for(const s of e.split(/[\s\n]/)){if(0"),p[2]+p[0].length);!h.test(p[0])&&12<=p[0].length?(f=function(t){let n="",o=!1,i=0,r=0,s=0;for(let e=0;e"),d=g}c.push("…");var m=e=c.join("");return 150]+>/g,"").length?e.substring(0,150)+"…":m}(t.doc.body,s.split(/\s+/)):t.doc.description||"",r.innerHTML=i;let e=t.ref;t.doc.body&&(e+="#:~:text="+(r=encodeURIComponent(s))),o.href=e,m.appendChild(n)}}),h.setAttribute("aria-expanded",0 div").forEach(e=>{e.removeEventListener("touchstart",c),e.addEventListener("touchstart",c)})},!0),document.addEventListener("keydown",function(t){var e=navigator.userAgent.toLowerCase().includes("mac")?t.metaKey:t.ctrlKey;if("k"===t.key&&e)t.preventDefault(),("block"===f.style.display?u:l)();else if(e=document.activeElement,"Tab"!==t.key||e!==h&&e!==p){if(0!==(r=m.querySelectorAll("#results > div")).length){var n,o,i=Array.from(r),r=m.querySelector('[aria-selected="true"]'),s=i.indexOf(r);if(["ArrowUp","ArrowDown","Home","End","PageUp","PageDown"].includes(t.key)){t.preventDefault();let e=s;switch(t.key){case"ArrowUp":e=Math.max(s-1,0);break;case"ArrowDown":e=Math.min(s+1,i.length-1);break;case"Home":e=0;break;case"End":e=i.length-1;break;case"PageUp":e=Math.max(s-3,0);break;case"PageDown":e=Math.min(s+3,i.length-1)}e!==s&&(a((o=i)[n=e]),o[n].scrollIntoView({block:"nearest",inline:"start"}))}"Enter"===t.key&&r&&(t.preventDefault(),t.stopImmediatePropagation(),(o=r.querySelector("a"))&&(window.location.href=o.getAttribute("href")),u())}}else t.preventDefault(),(e===h?p:h).focus()})}}; diff --git a/templates/partials/search_modal.html b/templates/partials/search_modal.html index 3b995511e..4077f1a37 100644 --- a/templates/partials/search_modal.html +++ b/templates/partials/search_modal.html @@ -15,15 +15,15 @@

{{ macros_translate::translate(key='
-
- 0 {{ macros_translate::translate(key='result', default='result', language_strings=language_strings) }} - {{ macros_translate::translate(key='results', default='results', language_strings=language_strings) }} + {#- Add the strings here so JavaScript can grab them -#} + {#- These are used in all languages -#} + {{ macros_translate::translate(key='results', number=0, default='No results', language_strings=language_strings, replace=false) }} + {{ macros_translate::translate(key='results', number=1, default='1 result', language_strings=language_strings, replace=false) }} + {{ macros_translate::translate(key='results', number=11, default='$NUMBER results', language_strings=language_strings, replace=false) }} + {#- Strings for specific languages -#} + {{ macros_translate::translate(key='results', number=2, default='$NUMBER results', language_strings=language_strings, replace=false) }} + {{ macros_translate::translate(key='results', number=2, default='$NUMBER results', language_strings=language_strings, replace=false) }}