diff --git a/include/search_renderer.h b/include/search_renderer.h index 2669b84cf..e328f85b5 100644 --- a/include/search_renderer.h +++ b/include/search_renderer.h @@ -72,6 +72,13 @@ class SearchRenderer this->pageLength = pageLength; } + /** + * set user language + */ + void setUserLang(const std::string& lang){ + this->userlang = lang; + } + /** * Generate the html page with the resutls of the search. * @@ -105,6 +112,7 @@ class SearchRenderer unsigned int pageLength; unsigned int estimatedResultCount; unsigned int resultStart; + std::string userlang = "en"; }; diff --git a/src/search_renderer.cpp b/src/search_renderer.cpp index 3b2ae161b..e64eb5611 100644 --- a/src/search_renderer.cpp +++ b/src/search_renderer.cpp @@ -32,9 +32,42 @@ #include "libkiwix-resources.h" #include "tools/stringTools.h" +#include "server/i18n.h" + namespace kiwix { +namespace +{ + +ParameterizedMessage searchResultsPageTitleMsg(const std::string& searchPattern) +{ + return ParameterizedMessage("search-results-page-title", + {{"SEARCH_PATTERN", searchPattern}} + ); +} + +ParameterizedMessage searchResultsPageHeaderMsg(const std::string& searchPattern, + const kainjow::mustache::data& r) +{ + if ( r.get("count")->string_value() == "0" ) { + return ParameterizedMessage("empty-search-results-page-header", + {{"SEARCH_PATTERN", searchPattern}} + ); + } else { + return ParameterizedMessage("search-results-page-header", + { + {"SEARCH_PATTERN", searchPattern}, + {"START", r.get("start")->string_value()}, + {"END", r.get("end") ->string_value()}, + {"COUNT", r.get("count")->string_value()}, + } + ); + } +} + +} // unnamed namespace + /* Constructor */ SearchRenderer::SearchRenderer(zim::SearchResultSet srs, unsigned int start, unsigned int estimatedResultCount) @@ -170,10 +203,20 @@ std::string SearchRenderer::renderTemplate(const std::string& tmpl_str, const Na result.set("absolutePath", absPathPrefix + urlEncode(path)); result.set("snippet", it.getSnippet()); if (library) { - result.set("bookTitle", library->getBookById(zim_id).getTitle()); + const std::string bookTitle = library->getBookById(zim_id).getTitle(); + const ParameterizedMessage bookInfoMsg("search-result-book-info", + {{"BOOK_TITLE", bookTitle}} + ); + result.set("bookInfo", bookInfoMsg.getText(userlang)); // for HTML + result.set("bookTitle", bookTitle); // for XML } if (it.getWordCount() >= 0) { - result.set("wordCount", kiwix::beautifyInteger(it.getWordCount())); + const auto wordCountStr = kiwix::beautifyInteger(it.getWordCount()); + const ParameterizedMessage wordCountMsg("word-count", + {{"COUNT", wordCountStr}} + ); + result.set("wordCountInfo", wordCountMsg.getText(userlang)); // for HTML + result.set("wordCount", wordCountStr); // for XML } items.push_back(result); @@ -181,7 +224,6 @@ std::string SearchRenderer::renderTemplate(const std::string& tmpl_str, const Na kainjow::mustache::data results; results.set("items", items); results.set("count", kiwix::beautifyInteger(estimatedResultCount)); - results.set("hasResults", estimatedResultCount != 0); results.set("start", kiwix::beautifyInteger(resultStart)); results.set("end", kiwix::beautifyInteger(std::min(resultStart+pageLength-1, estimatedResultCount))); @@ -198,12 +240,15 @@ std::string SearchRenderer::renderTemplate(const std::string& tmpl_str, const Na searchBookQuery ); - - kainjow::mustache::data allData; - allData.set("searchProtocolPrefix", searchProtocolPrefix); - allData.set("results", results); - allData.set("pagination", pagination); - allData.set("query", query); + const auto pageHeaderMsg = searchResultsPageHeaderMsg(searchPattern, results); + const kainjow::mustache::object allData{ + {"PAGE_TITLE", searchResultsPageTitleMsg(searchPattern).getText(userlang)}, + {"PAGE_HEADER", pageHeaderMsg.getText(userlang)}, + {"searchProtocolPrefix", searchProtocolPrefix}, + {"results", results}, + {"pagination", pagination}, + {"query", query}, + }; kainjow::mustache::mustache tmpl(tmpl_str); diff --git a/src/server/internalServer.cpp b/src/server/internalServer.cpp index 5c9d733d9..910ade8a4 100644 --- a/src/server/internalServer.cpp +++ b/src/server/internalServer.cpp @@ -967,6 +967,7 @@ std::unique_ptr InternalServer::handle_search_request(const RequestCon renderer.setProtocolPrefix(m_root + "/content/"); renderer.setSearchProtocolPrefix(m_root + "/search"); renderer.setPageLength(pageLength); + renderer.setUserLang(request.get_user_language()); if (request.get_requested_format() == "xml") { return ContentResponse::build( renderer.getXml(*mp_nameMapper, mp_library.get()), diff --git a/static/skin/i18n/en.json b/static/skin/i18n/en.json index 8c4a6b0bc..918304808 100644 --- a/static/skin/i18n/en.json +++ b/static/skin/i18n/en.json @@ -25,6 +25,11 @@ , "500-page-text": "An internal server error occured. We are sorry about that :/" , "fulltext-search-unavailable" : "Fulltext search unavailable" , "no-search-results": "The fulltext search engine is not available for this content." + , "search-results-page-title": "Search: {{SEARCH_PATTERN}}" + , "search-results-page-header": "Results {{START}}-{{END}} of {{COUNT}} for \"{{{SEARCH_PATTERN}}}\"" + , "empty-search-results-page-header": "No results were found for \"{{{SEARCH_PATTERN}}}\"" + , "search-result-book-info": "from {{BOOK_TITLE}}" + , "word-count": "{{COUNT}} words" , "library-button-text": "Go to welcome page" , "home-button-text": "Go to the main page of '{{BOOK_TITLE}}'" , "random-page-button-text": "Go to a randomly selected page" diff --git a/static/skin/i18n/qqq.json b/static/skin/i18n/qqq.json index 386ee1972..9a8c5c144 100644 --- a/static/skin/i18n/qqq.json +++ b/static/skin/i18n/qqq.json @@ -29,6 +29,11 @@ "500-page-text": "Text of the 500 error page", "fulltext-search-unavailable": "Title of the error page returned when search is attempted in a book without fulltext search database", "no-search-results": "Text of the error page returned when search is attempted in a book without fulltext search database", + "search-results-page-title": "Title of the search results page", + "search-results-page-header": "Header of the search results page", + "empty-search-results-page-header": "Header of the empty search results page", + "search-result-book-info": "Reference to the book where the search result belongs (this is displayed AFTER the search result)", + "word-count": "Word count information", "library-button-text": "Tooltip of the button leading to the welcome page", "home-button-text": "Tooltip of the button leading to the main page of a book", "random-page-button-text": "Tooltip of the button opening a randomly selected page", diff --git a/static/skin/i18n/test.json b/static/skin/i18n/test.json index 29def8eac..29c2e9613 100644 --- a/static/skin/i18n/test.json +++ b/static/skin/i18n/test.json @@ -42,4 +42,9 @@ , "preview-book": "[I18N] Preview [TESTING]" , "no-query" : "[I18N TESTING] Kiwix can read your thoughts but it is against GDPR. Please provide your query explicitly." , "invalid-request" : "[I18N TESTING] Invalid URL: \"{{{url}}}\"" + , "search-results-page-title": "[I18N TESTING] Search: {{SEARCH_PATTERN}}" + , "search-results-page-header": "[I18N TESTING] Results {{START}}-{{END}} of {{COUNT}} for \"{{{SEARCH_PATTERN}}}\"" + , "empty-search-results-page-header": "[I18N TESTING] No results were found for \"{{{SEARCH_PATTERN}}}\"" + , "search-result-book-info": "from [I18N TESTING] {{BOOK_TITLE}}" + , "word-count": "{{COUNT}} [I18N TESTING] words" } diff --git a/static/skin/languages.js b/static/skin/languages.js index bcddf8114..8585f9ec4 100644 --- a/static/skin/languages.js +++ b/static/skin/languages.js @@ -22,7 +22,7 @@ const uiLanguages = [ { "iso_code": "en", "self_name": "English", - "translation_count": 53 + "translation_count": 58 }, { "iso_code": "es", diff --git a/static/skin/viewer.js b/static/skin/viewer.js index a311552db..8beec989e 100644 --- a/static/skin/viewer.js +++ b/static/skin/viewer.js @@ -10,13 +10,22 @@ let viewerState = { uiLanguage: 'en', }; +function dropUserLang(query) { + const q = new URLSearchParams(query); + q.delete('userlang'); + const pre = (query.startsWith('?') && q.size != 0 ? '?' : ''); + return pre + q.toString(); +} + function userUrl2IframeUrl(url) { if ( url == '' ) { return blankPageUrl; } if ( url.startsWith('search?') ) { - return `${root}/${url}`; + const q = new URLSearchParams(url.slice("search?".length)); + q.set('userlang', viewerState.uiLanguage); + return `${root}/search?${q.toString()}`; } return `${root}/content/${url}`; @@ -73,7 +82,7 @@ function quasiUriEncode(s, specialSymbols) { function performSearch() { const searchbox = document.getElementById('kiwixsearchbox'); const q = encodeURIComponent(searchbox.value); - gotoUrl(`/search?books.name=${currentBook}&pattern=${q}`); + gotoUrl(`/search?books.name=${currentBook}&pattern=${q}&userlang=${viewerState.uiLanguage}`); } function makeJSLink(jsCodeString, linkText, linkAttr="") { @@ -148,7 +157,7 @@ function iframeUrl2UserUrl(url, query) { } if ( url == `${root}/search` ) { - return `search${query}`; + return `search${dropUserLang(query)}`; } url = url.slice(root.length); @@ -537,9 +546,8 @@ function setupViewer() { const lang = getUserLanguage(); setUserLanguage(lang, finishViewerSetupOnceTranslationsAreLoaded); viewerState.uiLanguage = lang; - const q = new URLSearchParams(window.location.search); - q.delete('userlang'); - const rewrittenURL = makeURL(q.toString(), location.hash); + const cleanedUpQuery = dropUserLang(window.location.search); + const rewrittenURL = makeURL(cleanedUpQuery, location.hash); history.replaceState(viewerState, null, rewrittenURL); kiwixToolBarWrapper.style.display = 'block'; diff --git a/static/templates/search_result.html b/static/templates/search_result.html index 93133c584..eac969158 100644 --- a/static/templates/search_result.html +++ b/static/templates/search_result.html @@ -102,23 +102,11 @@ } - Search: {{query.pattern}} + {{PAGE_TITLE}}
- {{#results.hasResults}} - Results - - {{results.start}}-{{results.end}} - of - {{results.count}} - for - "{{{query.pattern}}}" - - {{/results.hasResults}} - {{^results.hasResults}} - No results were found for "{{{query.pattern}}}" - {{/results.hasResults}} + {{{PAGE_HEADER}}}
@@ -131,12 +119,12 @@ {{#snippet}} {{>snippet}}... {{/snippet}} - {{#bookTitle}} -
from {{bookTitle}}
- {{/bookTitle}} - {{#wordCount}} -
{{wordCount}} words
- {{/wordCount}} + {{#bookInfo}} +
{{bookInfo}}
+ {{/bookInfo}} + {{#wordCountInfo}} +
{{wordCountInfo}}
+ {{/wordCountInfo}} {{/results.items}} diff --git a/test/server.cpp b/test/server.cpp index 5a0bd072e..fb9d577ea 100644 --- a/test/server.cpp +++ b/test/server.cpp @@ -75,7 +75,7 @@ const ResourceCollection resources200Compressible{ { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/taskbar.css" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/taskbar.css?cacheid=e014a885" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/viewer.js" }, - { STATIC_CONTENT, "/ROOT%23%3F/skin/viewer.js?cacheid=e9c025f2" }, + { STATIC_CONTENT, "/ROOT%23%3F/skin/viewer.js?cacheid=5fc4badf" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/fonts/Poppins.ttf" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/fonts/Poppins.ttf?cacheid=af705837" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/fonts/Roboto.ttf" }, @@ -84,7 +84,7 @@ const ResourceCollection resources200Compressible{ // TODO: implement cache management of i18n resources //{ STATIC_CONTENT, "/ROOT%23%3F/skin/i18n/test.json?cacheid=unknown" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/languages.js" }, - { STATIC_CONTENT, "/ROOT%23%3F/skin/languages.js?cacheid=c41aae47" }, + { STATIC_CONTENT, "/ROOT%23%3F/skin/languages.js?cacheid=9ccd43fd" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/catalog/search" }, @@ -286,7 +286,7 @@ R"EXPECTEDRESULT( href="/ROOT%23%3F/skin/kiwix.css?cacheid=2158fad9" - + @@ -319,8 +319,8 @@ R"EXPECTEDRESULT( - - + + const blankPageUrl = root + "/skin/blank.html?cacheid=6b1fa032"; @@ -1148,7 +1148,7 @@ R"EXPECTEDRESPONSE(const uiLanguages = [ { "iso_code": "en", "self_name": "English", - "translation_count": 53 + "translation_count": 58 }, { "iso_code": "es", diff --git a/test/server_search.cpp b/test/server_search.cpp index 358160edd..de8cd9046 100644 --- a/test/server_search.cpp +++ b/test/server_search.cpp @@ -113,7 +113,7 @@ std::string makeSearchResultsHtml(const std::string& pattern, } - Search: %PATTERN% + %USERLANGMARKER%Search: %PATTERN%
@@ -173,8 +173,8 @@ struct SearchResult + " " + title + "\n" + " \n" + " " + snippet + "\n" - + "
from " + bookTitle + "
\n" - + "
" + wordCount + " words
\n"; + + "
from %USERLANGMARKER%" + bookTitle + "
\n" + + "
" + wordCount + " %USERLANGMARKER%words
\n"; } std::string getXml() const @@ -737,26 +737,16 @@ struct TestData std::string expectedHtmlHeader() const { - if ( totalResultCount == 0 ) { - return "\n No results were found for \"" + getPattern() + "\""; - } - - std::string header = R"( Results - - FIRSTRESULT-LASTRESULT - of - RESULTCOUNT - for - "PATTERN" - - )"; + std::string header = totalResultCount == 0 + ? R"(No results were found for "PATTERN")" + : R"(Results FIRSTRESULT-LASTRESULT of RESULTCOUNT for "PATTERN")"; const size_t lastResultIndex = std::min(totalResultCount, firstResultIndex + results.size() - 1); header = replace(header, "FIRSTRESULT", std::to_string(firstResultIndex)); header = replace(header, "LASTRESULT", std::to_string(lastResultIndex)); header = replace(header, "RESULTCOUNT", std::to_string(totalResultCount)); header = replace(header, "PATTERN", getPattern()); - return header; + return "%USERLANGMARKER%" + header; } std::string expectedHtmlResultsString() const @@ -800,12 +790,18 @@ struct TestData std::string expectedHtml() const { - return makeSearchResultsHtml( - getPattern(), - expectedHtmlHeader(), - expectedHtmlResultsString(), - expectedHtmlFooter() + const std::string html = makeSearchResultsHtml( + getPattern(), + expectedHtmlHeader(), + expectedHtmlResultsString(), + expectedHtmlFooter() ); + + const std::string userlangMarker = extractQueryValue("userlang") == "test" + ? "[I18N TESTING] " + : ""; + + return replace(html, "%USERLANGMARKER%", userlangMarker); } std::string expectedXmlHeader() const @@ -824,7 +820,8 @@ struct TestData />)"; const auto realResultsPerPage = resultsPerPage?resultsPerPage:25; - const auto url = makeUrl(query + "&format=xml", firstResultIndex, realResultsPerPage); + const auto cleanedUpQuery = replace(query, "&userlang=test", ""); + const auto url = makeUrl(cleanedUpQuery + "&format=xml", firstResultIndex, realResultsPerPage); header = replace(header, "URL", replace(url, "&", "&")); header = replace(header, "FIRSTRESULT", std::to_string(firstResultIndex)); header = replace(header, "ITEMCOUNT", std::to_string(realResultsPerPage)); @@ -931,6 +928,17 @@ TEST(ServerSearchTest, searchResults) /* pagination */ {} }, + { + /* query */ "pattern=velomanyunkan&books.id=" RAYCHARLESZIMID + "&userlang=test", + /* start */ -1, + /* resultsPerPage */ 0, + /* totalResultCount */ 0, + /* firstResultIndex */ 1, + /* results */ {}, + /* pagination */ {} + }, + { /* query */ "pattern=razaf&books.id=" RAYCHARLESZIMID, /* start */ -1, @@ -1037,6 +1045,17 @@ TEST(ServerSearchTest, searchResults) /* pagination */ {} }, + { + /* query */ "pattern=jazz&books.id=" RAYCHARLESZIMID + "&userlang=test", + /* start */ -1, + /* resultsPerPage */ 100, + /* totalResultCount */ 44, + /* firstResultIndex */ 1, + /* results */ LARGE_SEARCH_RESULTS, + /* pagination */ {} + }, + { /* query */ "pattern=jazz&books.id=" RAYCHARLESZIMID, /* start */ -1,