Merge pull request #846 from kiwix/frontend_i18n

This commit is contained in:
Matthieu Gautier 2023-02-22 15:44:56 +01:00 committed by GitHub
commit 936707f73b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 403 additions and 102 deletions

View File

@ -97,14 +97,19 @@ inline std::string normalizeRootUrl(std::string rootUrl)
std::string
fullURL2LocalURL(const std::string& fullUrl, const std::string& rootLocation)
{
assert(rootLocation.size() > 0 && rootLocation.back() == '/');
if ( kiwix::startsWith(fullUrl, rootLocation) ) {
return fullUrl.substr(rootLocation.size() - 1);
return fullUrl.substr(rootLocation.size());
} else {
return "";
return "INVALID URL";
}
}
std::string getSearchComponent(const RequestContext& request)
{
const std::string query = request.get_query();
return query.empty() ? query : "?" + query;
}
Filter get_search_filter(const RequestContext& request, const std::string& prefix="")
{
auto filter = kiwix::Filter().valid(true).local(true);
@ -415,7 +420,7 @@ InternalServer::InternalServer(Library* library,
m_addr(addr),
m_port(port),
m_root(normalizeRootUrl(root)),
m_rootPrefixOfDecodedURL(m_root + "/"),
m_rootPrefixOfDecodedURL(m_root),
m_nbThreads(nbThreads),
m_multizimSearchLimit(multizimSearchLimit),
m_verbose(verbose),
@ -585,6 +590,13 @@ std::unique_ptr<Response> InternalServer::handle_request(const RequestContext& r
+ urlNotFoundMsg;
}
if ( request.get_url() == "" ) {
// Redirect /ROOT_LOCATION to /ROOT_LOCATION/ (note the added slash)
// so that relative URLs are resolved correctly
const std::string query = getSearchComponent(request);
return Response::build_redirect(*this, m_root + "/" + query);
}
const ETag etag = get_matching_if_none_match_etag(request, getLibraryId());
if ( etag )
return Response::build_304(*this, etag);
@ -623,11 +635,9 @@ std::unique_ptr<Response> InternalServer::handle_request(const RequestContext& r
if (isEndpointUrl(url, "catch"))
return handle_catch(request);
std::string contentUrl = m_root + "/content" + urlEncode(url);
const std::string query = request.get_query();
if ( ! query.empty() )
contentUrl += "?" + query;
return Response::build_redirect(*this, contentUrl);
const std::string contentUrl = m_root + "/content" + urlEncode(url);
const std::string query = getSearchComponent(request);
return Response::build_redirect(*this, contentUrl + query);
} catch (std::exception& e) {
fprintf(stderr, "===== Unhandled error : %s\n", e.what());
return HTTP500Response(*this, request)

View File

@ -181,7 +181,7 @@ std::string RequestContext::get_root_path() const {
}
bool RequestContext::is_valid_url() const {
return !url.empty();
return url.empty() || url[0] == '/';
}
ByteRange RequestContext::get_range() const {

View File

@ -97,6 +97,62 @@ function setUserLanguage(lang, callback) {
Translations.whenReady(callback);
}
function createModalUILanguageSelector() {
document.body.insertAdjacentHTML('beforeend',
`<div id="uiLanguageSelector" class="modal-wrapper">
<div class="modal">
<div class="modal-heading">
<div class="modal-title">
<div>
Select UI language
</div>
</div>
<div onclick="window.modalUILanguageSelector.close()" class="modal-close-button">
<div>
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14" fill="none">
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.7071 1.70711C14.0976 1.31658 14.0976
0.683417 13.7071 0.292893C13.3166 -0.0976311 12.6834 -0.0976311 12.2929 0.292893L7 5.58579L1.70711
0.292893C1.31658 -0.0976311 0.683417 -0.0976311 0.292893 0.292893C-0.0976311 0.683417
-0.0976311 1.31658 0.292893 1.70711L5.58579 7L0.292893 12.2929C-0.0976311 12.6834
-0.0976311 13.3166 0.292893 13.7071C0.683417 14.0976 1.31658 14.0976 1.70711 13.7071L7
8.41421L12.2929 13.7071C12.6834 14.0976 13.3166 14.0976 13.7071 13.7071C14.0976 13.3166
14.0976 12.6834 13.7071 12.2929L8.41421 7L13.7071 1.70711Z" fill="black" />
</svg>
</div>
</div>
</div>
<div class="modal-content">
<select id="ui_language"></select>
</div>
</div>
</div>`);
window.modalUILanguageSelector = {
show: () => {
document.getElementById('uiLanguageSelector').style.display = 'flex';
},
close: () => {
document.getElementById('uiLanguageSelector').style.display = 'none';
}
};
}
function initUILanguageSelector(activeLanguage, languageChangeCallback) {
if ( document.getElementById("ui_language") == null ) {
createModalUILanguageSelector();
}
const languageSelector = document.getElementById("ui_language");
for (const lang of uiLanguages ) {
const lang_name = Object.getOwnPropertyNames(lang)[0];
const lang_code = lang[lang_name];
const is_selected = lang_code == activeLanguage;
languageSelector.appendChild(new Option(lang_name, lang_code, is_selected, is_selected));
}
languageSelector.onchange = languageChangeCallback;
}
window.$t = $t;
window.getUserLanguage = getUserLanguage;
window.setUserLanguage = setUserLanguage;
window.initUILanguageSelector = initUILanguageSelector;

View File

@ -28,4 +28,22 @@
, "random-page-button-text": "Go to a randomly selected page"
, "searchbox-tooltip": "Search '{{BOOK_TITLE}}'"
, "confusion-of-tongues": "Two or more books in different languages would participate in search, which may lead to confusing results."
, "welcome-page-overzealous-filter": "No result. Would you like to <a href=\"?lang=\">reset filter</a>?"
, "powered-by-kiwix-html": "Powered by&nbsp;<a href=\"https://kiwix.org\">Kiwix</a>"
, "search": "Search"
, "book-filtering-all-categories": "All categories"
, "book-filtering-all-languages": "All languages"
, "count-of-matching-books": "{{COUNT}} book(s)"
, "download": "Download"
, "direct-download-link-text": "Direct"
, "direct-download-alt-text": "direct download"
, "hash-download-link-text": "Sha256 hash"
, "hash-download-alt-text": "download hash"
, "magnet-link-text": "Magnet link"
, "magnet-alt-text": "download magnet"
, "torrent-download-link-text": "Torrent file"
, "torrent-download-alt-text": "download torrent"
, "library-opds-feed": "Library OPDS Feed"
, "filter-by-tag": "Filter by tag \"{{TAG}}\""
, "stop-filtering-by-tag": "Stop filtering by tag \"{{TAG}}\""
}

View File

@ -29,5 +29,23 @@
"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",
"searchbox-tooltip": "Tooltip displayed for the search box"
"searchbox-tooltip": "Tooltip displayed for the search box in the viewer"
, "welcome-page-overzealous-filter": "Text shown when book filtering on the welcome page produces zero results"
, "powered-by-kiwix-html": "Link to Kiwix website"
, "search": "A general search action (text displayed on search buttons or as aplaceholder in searchboxes)"
, "book-filtering-all-categories": "Choosing this filter will disable filtering of books by category"
, "book-filtering-all-languages": "Choosing this filter will disable filtering of books by language"
, "count-of-matching-books": "Reporting the count of books matching the filter"
, "download": "A general download action"
, "direct-download-link-text": "Link text for a direct download"
, "direct-download-alt-text": "Hint for a direct download icon"
, "hash-download-link-text": "Link text for downloading the hash"
, "hash-download-alt-text": "Hint for the icon of hash download"
, "magnet-link-text": "Link text for a magnet link"
, "magnet-alt-text": "Hint for the icon of a magnet link"
, "torrent-download-link-text": "Link text for downloading the torrent file"
, "torrent-download-alt-text": "Hint for the icon of torrent download"
, "library-opds-feed": "Hint for the library OPDS feed link"
, "filter-by-tag": "Hint for a link that would load results filtered by a single tag"
, "stop-filtering-by-tag": "Tooltip for the button that cancels filtering by tag"
}

View File

@ -17,4 +17,22 @@
, "home-button-text": "[I18N TESTING] Jump to the main page of '{{BOOK_TITLE}}'"
, "random-page-button-text": "[I18N TESTING] I am tired of determinism"
, "searchbox-tooltip": "[I18N TESTING] Let's search in '{{BOOK_TITLE}}'"
, "welcome-page-overzealous-filter": "[I18N TESTING] Nothing found. <a href=\"?lang=\">Reset filter</a>"
, "powered-by-kiwix-html": "[I18N TESTING] Powered by&nbsp;<a href=\"https://kiwix.org\">Kiwix</a> (nominal power: 1.23 kW)"
, "search": "[I18N Search TESTING]"
, "book-filtering-all-categories": "All [I18N TESTING] categories"
, "book-filtering-all-languages": "All [I18N TESTING] languages"
, "count-of-matching-books": "[I18N TESTING] Number of matching books: {{COUNT}}"
, "download": "[I18N Download TESTING]"
, "direct-download-link-text": "[I18N TESTING] HTTP(S)"
, "direct-download-alt-text": "[I18N TESTING] download directly"
, "hash-download-link-text": "Sha256 [I18N TESTING] hash"
, "hash-download-alt-text": "download [I18N TESTING] hash"
, "magnet-link-text": "Magnet [I18N TESTING] link"
, "magnet-alt-text": "download [I18N TESTING] magnet"
, "torrent-download-link-text": "Torrent [I18N TESTING] file"
, "torrent-download-alt-text": "download [I18N TESTING] torrent"
, "library-opds-feed": "Library [I18N] OPDS [TESTING] Feed"
, "filter-by-tag": "Filter [I18N] by [TESTING] tag \"{{TAG}}\""
, "stop-filtering-by-tag": "[I18N] Stop filtering [TESTING] by tag \"{{TAG}}\""
}

View File

@ -157,6 +157,28 @@ body {
font-weight: bolder;
}
#uiLanguageSelector {
display: none;
}
#uiLanguageSelector .modal {
height: 140px;
}
#uiLanguageSelector .modal-heading {
height: 40%;
}
#uiLanguageSelector .modal-content #ui_language {
font-size: 1.6rem;
width: 100%;
}
#uiLanguageSelectorButton {
margin: 16px 12px 0 0;
float: right;
}
.book__list {
position: relative;
margin: 0 auto;
@ -489,8 +511,4 @@ body {
.kiwixNav__filters {
grid-template-columns: 1fr;
}
.feedLogo {
display: none;
}
}

View File

@ -15,6 +15,7 @@
let noResultInjected = false;
let filters = getCookie(filterCookieName);
let params = new URLSearchParams(window.location.search || filters || '');
params.delete('userlang');
let timer;
let languages = {};
@ -31,6 +32,14 @@
document.querySelector('#feedLink').href = feedLink;
}
function changeUILanguage() {
window.modalUILanguageSelector.close();
const s = document.getElementById("ui_language");
const lang = s.options[s.selectedIndex].value;
setPermanentGlobalCookie('userlang', lang);
window.location.reload();
}
function queryUrlBuilder() {
let url = `${root}/catalog/search?`;
url += Object.keys(incrementalLoadingParams).map(key => `${key}=${incrementalLoadingParams[key]}`).join("&");
@ -38,10 +47,14 @@
return (url);
}
function setCookie(cookieName, cookieValue) {
const date = new Date();
date.setTime(date.getTime() + oneDayDelta);
document.cookie = `${cookieName}=${cookieValue};expires=${date.toUTCString()};sameSite=Strict`;
function setCookie(cookieName, cookieValue, ttl) {
let exp = "";
if ( ttl ) {
const date = new Date();
date.setTime(date.getTime() + ttl);
exp = `expires=${date.toUTCString()};`;
}
document.cookie = `${cookieName}=${cookieValue};${exp}sameSite=Strict`;
}
function getCookie(cookieName) {
@ -93,7 +106,7 @@
function generateTagLink(tagValue) {
tagValue = tagValue.toLowerCase();
const humanFriendlyTagValue = humanFriendlyTitle(tagValue);
const tagMessage = `Filter by tag "${humanFriendlyTagValue}"`;
const tagMessage = $t("filter-by-tag", {TAG: humanFriendlyTagValue});
return `<span class='tag__link' aria-label='${tagMessage}' title='${tagMessage}' data-tag=${tagValue}>${humanFriendlyTagValue}</span>`
}
@ -143,7 +156,7 @@
<div class="book__icon" ${faviconAttr}></div>
<div class="book__header">
<div id="book__title">${title}</div>
${downloadLink ? `<div class="book__download"><span data-link="${downloadLink}">Download ${humanFriendlyZimSize ? ` - ${humanFriendlyZimSize}</span></div>`: ''}` : ''}
${downloadLink ? `<div class="book__download"><span data-link="${downloadLink}">${$t("download")} ${humanFriendlyZimSize ? ` - ${humanFriendlyZimSize}</span></div>`: ''}` : ''}
</div>
<div class="book__description" title="${description}">${description}</div>
</div>
@ -209,27 +222,27 @@
<div class="modal-content">
<div class="modal-regular-download">
<a href="${downloadLink}" download>
<img src="../skin/download.png?KIWIXCACHEID" alt="direct download" />
<div>Direct</div>
<img src="${root}/skin/download.png?KIWIXCACHEID" alt="${$t("direct-download-alt-text")}" />
<div>${$t("direct-download-link-text")}</div>
</a>
</div>
<div class="modal-regular-download">
<a href="${downloadLink}.sha256" download>
<img src="../skin/hash.png?KIWIXCACHEID" alt="download hash" />
<div>Sha256 hash</div>
<img src="${root}/skin/hash.png?KIWIXCACHEID" alt="${$t("hash-download-alt-text")}" />
<div>${$t("hash-download-link-text")}</div>
</a>
</div>
${magnetLink ?
`<div class="modal-regular-download">
<a href="${magnetLink}" target="_blank">
<img src="../skin/magnet.png?KIWIXCACHEID" alt="download magnet" />
<div>Magnet link</div>
<img src="${root}/skin/magnet.png?KIWIXCACHEID" alt="${$t("magnet-alt-text")}" />
<div>${$t("magnet-link-text")}</div>
</a>
</div>` : ``}
<div class="modal-regular-download">
<a href="${downloadLink}.torrent" download>
<img src="../skin/bittorrent.png?KIWIXCACHEID" alt="download torrent" />
<div>Torrent file</div>
<img src="${root}/skin/bittorrent.png?KIWIXCACHEID" alt="${$t("torrent-download-alt-text")}" />
<div>${$t("torrent-download-link-text")}</div>
</a>
</div>
</div>
@ -262,16 +275,10 @@
} else {
toggleFooter();
}
const kiwixResultText = document.querySelector('.kiwixHomeBody__results')
if (results) {
let resultText = `${results} books`;
if (results === 1) {
resultText = `${results} book`;
}
kiwixResultText.innerHTML = resultText;
} else {
kiwixResultText.innerHTML = ``;
}
const text = results
? $t("count-of-matching-books", {COUNT: results})
: '';
document.querySelector('.kiwixHomeBody__results').innerHTML = text;
loader.style.display = 'none';
return books;
});
@ -298,7 +305,7 @@
const kiwixHomeBody = document.querySelector('.kiwixHomeBody');
const divTag = document.createElement('div');
divTag.setAttribute('class', 'noResults');
divTag.innerHTML = `No result. Would you like to <a href="?lang=">reset filter</a>?`;
divTag.innerHTML = $t("welcome-page-overzealous-filter");
kiwixHomeBody.append(divTag);
kiwixHomeBody.setAttribute('style', 'display: flex; justify-content: center; align-items: center');
loader.setAttribute('style', 'position: absolute; top: 50%');
@ -373,7 +380,7 @@
if (filterType) {
params.set(filterType, filterValue);
window.history.pushState({}, null, `?${params.toString()}`);
setCookie(filterCookieName, params.toString());
setCookie(filterCookieName, params.toString(), oneDayDelta);
}
updateFilterColors();
updateFeedLink();
@ -411,7 +418,7 @@
tagElement.style.display = 'inline-block';
const humanFriendlyTagValue = humanFriendlyTitle(tagValue);
tagElement.innerHTML = `${humanFriendlyTagValue}`;
const tagMessage = `Stop filtering by tag "${humanFriendlyTagValue}"`;
const tagMessage = $t("stop-filtering-by-tag", {TAG: humanFriendlyTagValue});
tagElement.setAttribute('aria-label', tagMessage);
tagElement.setAttribute('title', tagMessage);
if (resetFilter)
@ -462,7 +469,22 @@
}
});
window.onload = async () => {
function updateUIText() {
footer.innerHTML = $t("powered-by-kiwix-html");
const searchText = $t("search");
document.getElementById('searchFilter').placeholder = searchText;
document.getElementById('searchButton').value = searchText;
document.getElementById('categoryFilter').children[0].innerHTML = $t("book-filtering-all-categories");
document.getElementById('languageFilter').children[0].innerHTML = $t("book-filtering-all-languages");
const feedLogoElem = document.getElementById('feedLogo');
const libraryOpdsFeedHint = $t("library-opds-feed");
for (const attr of ["alt", "aria-label", "title"] ) {
feedLogoElem.setAttribute(attr, libraryOpdsFeedHint);
}
}
async function onload() {
initUILanguageSelector(getUserLanguage(), changeUILanguage);
iso = new Isotope( '.book__list', {
itemSelector: '.book',
getSortData:{
@ -478,6 +500,7 @@
}
});
footer = document.getElementById('kiwixfooter');
updateUIText();
fadeOutDiv = document.getElementById('fadeOut');
loader = document.querySelector('.loader');
await loadAndDisplayOptions('#languageFilter', `${root}/catalog/v2/languages`, 'language');
@ -507,7 +530,14 @@
}
}
updateFeedLink();
setCookie(filterCookieName, params.toString());
setCookie(filterCookieName, params.toString(), oneDayDelta);
};
// required by i18n.js:setUserLanguage()
window.setPermanentGlobalCookie = function(name, value) {
document.cookie = `${name}=${value};path=${root};max-age=31536000`;
}
window.onload = () => { setUserLanguage(getUserLanguage(), onload); }
})();

View File

@ -44,10 +44,6 @@
margin: 0 auto;
}
.kiwix #ui_language {
float: left;
}
#kiwix_button_show_toggle {
display: none;
}
@ -84,7 +80,6 @@ label[for="kiwix_button_show_toggle"],
float: right;
}
.kiwix #ui_language,
.kiwix #kiwixtoolbar button,
.kiwix #kiwixtoolbar input[type="submit"] {
box-sizing: border-box !important;
@ -134,6 +129,79 @@ a.suggest, a.suggest:visited, a.suggest:hover, a.suggest:active {
column-count: 1 !important;
}
.modal-wrapper {
position: fixed;
z-index: 100;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
align-content: center;
background-color: rgba(0, 0, 0, 30%);
}
.modal {
color: #444343;
height: 280px;
width: 250px;
margin: 15px;
background-color: #f7f7f7;
border: 1px solid #ececec;
border-radius: 3px;
}
.modal-heading {
background-color: #f0f0f0;
height: 20%;
width: 100%;
border-bottom: 1px solid #ececec;
display: grid;
grid-template-columns: 3fr 1fr;
}
.modal-title {
display: flex;
font-size: 15px;
align-items: center;
padding-left: 20px;
font-family: poppins;
}
.modal-close-button {
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
}
.modal-content {
padding: 20px;
}
#uiLanguageSelector {
display: none;
}
#uiLanguageSelector .modal {
height: 140px;
}
#uiLanguageSelector .modal-heading {
height: 40%;
}
#uiLanguageSelector .modal-content #ui_language {
width: 100%;
}
#uiLanguageSelectorButton {
margin: 0px 12px 6px 12px;
float: right;
}
@media(min-width:420px) {
.kiwix_button_cont {
display: inline-block !important;

View File

@ -416,16 +416,6 @@ function makeURL(search, hash) {
return url;
}
function initUILanguageSelector() {
const languageSelector = document.getElementById("ui_language");
for (const lang of uiLanguages ) {
const lang_name = Object.getOwnPropertyNames(lang)[0];
const lang_code = lang[lang_name];
const is_selected = lang_code == viewerState.uiLanguage;
languageSelector.appendChild(new Option(lang_name, lang_code, is_selected, is_selected));
}
}
function updateUILanguageSelector(userLang) {
console.log(`updateUILanguageSelector(${userLang})`);
const languageSelector = document.getElementById("ui_language");
@ -446,6 +436,7 @@ function handle_history_state_change(event) {
}
function changeUILanguage() {
window.modalUILanguageSelector.close();
const s = document.getElementById("ui_language");
const lang = s.options[s.selectedIndex].value;
viewerState.uiLanguage = lang;
@ -481,7 +472,7 @@ function setupViewer() {
document.getElementById("kiwix_serve_taskbar_library_button").remove();
}
initUILanguageSelector();
initUILanguageSelector(viewerState.uiLanguage, changeUILanguage);
setupSuggestions();
// cybook hack

View File

@ -37,13 +37,31 @@
src: url("{{root}}/skin/fonts/Roboto.ttf?KIWIXCACHEID") format("truetype");
}
</style>
<script type="module" src="{{root}}/skin/i18n.js?KIWIXCACHEID" defer></script>
<script type="text/javascript" src="{{root}}/skin/languages.js?KIWIXCACHEID" defer></script>
<script src="{{root}}/skin/isotope.pkgd.min.js?KIWIXCACHEID" defer></script>
<script src="{{root}}/skin/iso6391To3.js?KIWIXCACHEID"></script>
<script type="text/javascript" src="{{root}}/skin/index.js?KIWIXCACHEID" defer></script>
</head>
<body>
<a href="{{root}}/catalog/v2/entries" id="feedLink">
<img src="{{root}}/skin/feed.png?KIWIXCACHEID" class="feedLogo" alt="Library OPDS Feed" aria-label="Library OPDS Feed" title="Library OPDS Feed">
<img src="{{root}}/skin/feed.png?KIWIXCACHEID"
class="feedLogo"
id="feedLogo"
alt="Library OPDS Feed"
aria-label="Library OPDS Feed"
title="Library OPDS Feed">
</a>
<a onclick="window.modalUILanguageSelector.show()">
<svg xmlns="http://www.w3.org/2000/svg"
id="uiLanguageSelectorButton"
width="30"
height="30"
viewBox="0 0 20 20">
<g fill="#36c">
<path d="M20 18h-1.44a.61.61 0 0 1-.4-.12.81.81 0 0 1-.23-.31L17 15h-5l-1 2.54a.77.77 0 0 1-.22.3.59.59 0 0 1-.4.14H9l4.55-11.47h1.89zm-3.53-4.31L14.89 9.5a11.62 11.62 0 0 1-.39-1.24q-.09.37-.19.69l-.19.56-1.58 4.19zm-6.3-1.58a13.43 13.43 0 0 1-2.91-1.41 11.46 11.46 0 0 0 2.81-5.37H12V4H7.31a4 4 0 0 0-.2-.56C6.87 2.79 6.6 2 6.6 2l-1.47.5s.4.89.6 1.5H0v1.33h2.15A11.23 11.23 0 0 0 5 10.7a17.19 17.19 0 0 1-5 2.1q.56.82.87 1.38a23.28 23.28 0 0 0 5.22-2.51 15.64 15.64 0 0 0 3.56 1.77zM3.63 5.33h4.91a8.11 8.11 0 0 1-2.45 4.45 9.11 9.11 0 0 1-2.46-4.45z"/>
</g>
</svg>
</a>
<div class='kiwixNav'>
<div class="kiwixNav__filters">
@ -61,7 +79,7 @@
<form id='kiwixSearchForm' class='kiwixNav__SearchForm'>
<input type="text" name="q" placeholder="Search" id="searchFilter" class='kiwixSearch filter'>
<span class="kiwixButton tagFilterLabel"></span>
<input type="submit" class="kiwixButton kiwixButtonHover" value="Search"/>
<input type="submit" class="kiwixButton kiwixButtonHover" id="searchButton" value="Search"/>
</form>
</div>
<div class="kiwixHomeBody">
@ -76,7 +94,11 @@
<script>
function closeModal() {
for(modal of document.getElementsByClassName('modal-wrapper')) {
modal.remove();
if ( modal.id == "uiLanguageSelector" ) {
window.modalUILanguageSelector.close();
} else {
modal.remove();
}
}
}
</script>

View File

@ -29,9 +29,17 @@
<body style="margin:0" onload="setupViewer()">
<div class="kiwix" style="display:none" id="kiwixtoolbarwrapper">
<div id="kiwixtoolbar" class="ui-widget-header">
<select id="ui_language" class="kiwix" onchange="changeUILanguage()">
</select>
<div class="kiwix_centered">
<a id="uiLanguageSelectorButton" onclick="window.modalUILanguageSelector.show()">
<svg xmlns="http://www.w3.org/2000/svg"
width="30"
height="30"
viewBox="0 0 20 20">
<g fill="#36c">
<path d="M20 18h-1.44a.61.61 0 0 1-.4-.12.81.81 0 0 1-.23-.31L17 15h-5l-1 2.54a.77.77 0 0 1-.22.3.59.59 0 0 1-.4.14H9l4.55-11.47h1.89zm-3.53-4.31L14.89 9.5a11.62 11.62 0 0 1-.39-1.24q-.09.37-.19.69l-.19.56-1.58 4.19zm-6.3-1.58a13.43 13.43 0 0 1-2.91-1.41 11.46 11.46 0 0 0 2.81-5.37H12V4H7.31a4 4 0 0 0-.2-.56C6.87 2.79 6.6 2 6.6 2l-1.47.5s.4.89.6 1.5H0v1.33h2.15A11.23 11.23 0 0 0 5 10.7a17.19 17.19 0 0 1-5 2.1q.56.82.87 1.38a23.28 23.28 0 0 0 5.22-2.51 15.64 15.64 0 0 0 3.56 1.77zM3.63 5.33h4.91a8.11 8.11 0 0 1-2.45 4.45 9.11 9.11 0 0 1-2.46-4.45z"/>
</g>
</svg>
</a>
<div class="kiwix_searchform">
<form class="kiwixsearch" method="GET" action="javascript:performSearch()" id="kiwixsearchform">
<label for="kiwixsearchbox">&#x1f50d;</label>

View File

@ -59,11 +59,11 @@ const ResourceCollection resources200Compressible{
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/css/autoComplete.css" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/css/autoComplete.css?cacheid=08951e06" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/i18n.js" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/i18n.js?cacheid=6da2bca0" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/i18n.js?cacheid=2cf0f8c5" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/index.css" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/index.css?cacheid=316dbc21" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/index.css?cacheid=f0ee124c" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/index.js" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/index.js?cacheid=b0cc9d6b" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/index.js?cacheid=042058df" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/iso6391To3.js" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/iso6391To3.js?cacheid=ecde2bb3" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/isotope.pkgd.min.js" },
@ -71,13 +71,16 @@ const ResourceCollection resources200Compressible{
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/mustache.min.js" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/mustache.min.js?cacheid=bd23c4fb" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/taskbar.css" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/taskbar.css?cacheid=eb3bec90" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/taskbar.css?cacheid=8fc2cc83" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/viewer.js" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/viewer.js?cacheid=03fd97ee" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/viewer.js?cacheid=b9a574d4" },
{ 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" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/fonts/Roboto.ttf?cacheid=84d10248" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/i18n/test.json" },
// TODO: implement cache management of i18n resources
//{ STATIC_CONTENT, "/ROOT%23%3F/skin/i18n/test.json?cacheid=unknown" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/catalog/search" },
@ -141,9 +144,6 @@ const ResourceCollection resources200Uncompressible{
{ STATIC_CONTENT, "/ROOT%23%3F/skin/search-icon.svg?cacheid=b10ae7ed" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/search_results.css" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/search_results.css?cacheid=76d39c84" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/i18n/test.json" },
// 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=fe100348" },
@ -270,7 +270,7 @@ TEST_F(ServerTest, CacheIdsOfStaticResources)
const std::vector<UrlAndExpectedResult> testData{
{
/* url */ "/ROOT%23%3F/",
R"EXPECTEDRESULT( href="/ROOT%23%3F/skin/index.css?cacheid=316dbc21"
R"EXPECTEDRESULT( href="/ROOT%23%3F/skin/index.css?cacheid=f0ee124c"
<link rel="apple-touch-icon" sizes="180x180" href="/ROOT%23%3F/skin/favicon/apple-touch-icon.png?cacheid=f86f8df3">
<link rel="icon" type="image/png" sizes="32x32" href="/ROOT%23%3F/skin/favicon/favicon-32x32.png?cacheid=79ded625">
<link rel="icon" type="image/png" sizes="16x16" href="/ROOT%23%3F/skin/favicon/favicon-16x16.png?cacheid=a986fedc">
@ -280,10 +280,12 @@ R"EXPECTEDRESULT( href="/ROOT%23%3F/skin/index.css?cacheid=316dbc21"
<meta name="msapplication-config" content="/ROOT%23%3F/skin/favicon/browserconfig.xml?cacheid=f29a7c4a">
src: url("/ROOT%23%3F/skin/fonts/Poppins.ttf?cacheid=af705837") format("truetype");
src: url("/ROOT%23%3F/skin/fonts/Roboto.ttf?cacheid=84d10248") format("truetype");
<script type="module" src="/ROOT%23%3F/skin/i18n.js?cacheid=2cf0f8c5" defer></script>
<script type="text/javascript" src="/ROOT%23%3F/skin/languages.js?cacheid=fe100348" defer></script>
<script src="/ROOT%23%3F/skin/isotope.pkgd.min.js?cacheid=2e48d392" defer></script>
<script src="/ROOT%23%3F/skin/iso6391To3.js?cacheid=ecde2bb3"></script>
<script type="text/javascript" src="/ROOT%23%3F/skin/index.js?cacheid=b0cc9d6b" defer></script>
<img src="/ROOT%23%3F/skin/feed.png?cacheid=56a672b1" class="feedLogo" alt="Library OPDS Feed" aria-label="Library OPDS Feed" title="Library OPDS Feed">
<script type="text/javascript" src="/ROOT%23%3F/skin/index.js?cacheid=042058df" defer></script>
<img src="/ROOT%23%3F/skin/feed.png?cacheid=56a672b1"
)EXPECTEDRESULT"
},
{
@ -293,19 +295,19 @@ R"EXPECTEDRESULT( background-image: url('../skin/search-icon.svg?cacheid=b10a
},
{
/* url */ "/ROOT%23%3F/skin/index.js",
R"EXPECTEDRESULT( <img src="../skin/download.png?cacheid=a39aa502" alt="direct download" />
<img src="../skin/hash.png?cacheid=f836e872" alt="download hash" />
<img src="../skin/magnet.png?cacheid=73b6bddf" alt="download magnet" />
<img src="../skin/bittorrent.png?cacheid=4f5c6882" alt="download torrent" />
R"EXPECTEDRESULT( <img src="${root}/skin/download.png?cacheid=a39aa502" alt="${$t("direct-download-alt-text")}" />
<img src="${root}/skin/hash.png?cacheid=f836e872" alt="${$t("hash-download-alt-text")}" />
<img src="${root}/skin/magnet.png?cacheid=73b6bddf" alt="${$t("magnet-alt-text")}" />
<img src="${root}/skin/bittorrent.png?cacheid=4f5c6882" alt="${$t("torrent-download-alt-text")}" />
)EXPECTEDRESULT"
},
{
/* url */ "/ROOT%23%3F/viewer",
R"EXPECTEDRESULT( <link type="text/css" href="./skin/taskbar.css?cacheid=eb3bec90" rel="Stylesheet" />
R"EXPECTEDRESULT( <link type="text/css" href="./skin/taskbar.css?cacheid=8fc2cc83" rel="Stylesheet" />
<link type="text/css" href="./skin/css/autoComplete.css?cacheid=08951e06" rel="Stylesheet" />
<script type="module" src="./skin/i18n.js?cacheid=6da2bca0" defer></script>
<script type="module" src="./skin/i18n.js?cacheid=2cf0f8c5" defer></script>
<script type="text/javascript" src="./skin/languages.js?cacheid=fe100348" defer></script>
<script type="text/javascript" src="./skin/viewer.js?cacheid=03fd97ee" defer></script>
<script type="text/javascript" src="./skin/viewer.js?cacheid=b9a574d4" defer></script>
<script type="text/javascript" src="./skin/autoComplete.min.js?cacheid=1191aaaf"></script>
const blankPageUrl = root + "/skin/blank.html?cacheid=6b1fa032";
<label for="kiwix_button_show_toggle"><img src="./skin/caret.png?cacheid=22b942b4" alt=""></label>
@ -358,6 +360,10 @@ TEST_F(ServerTest, 400)
const char* urls404[] = {
"/",
"/zimfile",
"/ROOT",
"/ROOT%23%",
"/ROOT%23%3",
"/ROOT%23%3Fxyz",
"/ROOT%23%3F/skin/non-existent-skin-resource",
"/ROOT%23%3F/skin/autoComplete.min.js?cacheid=wrongcacheid",
"/ROOT%23%3F/catalog",
@ -1267,6 +1273,36 @@ TEST_F(ServerTest, UserLanguageControl)
}
}
TEST_F(ServerTest, SlashlessRootURLIsRedirectedToSlashfulURL)
{
const std::pair<const char*, const char*> test_data[] = {
// URL redirect
{ "/ROOT%23%3F", "/ROOT%23%3F/" },
{ "/ROOT%23%3F?abcd=123&xyz=890", "/ROOT%23%3F/?abcd=123&xyz=890" }
};
for ( const auto& t : test_data )
{
const TestContext ctx{ {"url", t.first} };
const auto g = zfs1_->GET(t.first);
ASSERT_EQ(302, g->status) << ctx;
ASSERT_TRUE(g->has_header("Location")) << ctx;
ASSERT_EQ(g->get_header_value("Location"), t.second) << ctx;
ASSERT_EQ(getCacheControlHeader(*g), "max-age=0, must-revalidate") << ctx;
ASSERT_FALSE(g->has_header("ETag")) << ctx;
}
}
TEST_F(ServerTest, EmptyRootIsNotRedirected)
{
ZimFileServer::Cfg serverCfg;
serverCfg.root = "";
resetServer(serverCfg);
ASSERT_EQ(200, zfs1_->GET("/")->status);
}
TEST_F(ServerTest, RandomPageRedirectsToAnExistingArticle)
{
auto g = zfs1_->GET("/ROOT%23%3F/random?content=zimfile");

View File

@ -68,10 +68,18 @@ public: // types
DEFAULT_OPTIONS = WITH_TASKBAR | WITH_LIBRARY_BUTTON
};
struct Cfg
{
std::string root = "ROOT#?";
Options options = DEFAULT_OPTIONS;
Cfg(Options opts = DEFAULT_OPTIONS) : options(opts) {}
};
public: // functions
ZimFileServer(int serverPort, Options options, std::string libraryFilePath);
ZimFileServer(int serverPort, Cfg cfg, std::string libraryFilePath);
ZimFileServer(int serverPort,
Options options,
Cfg cfg,
const FilePathCollection& zimpaths,
std::string indexTemplateString = "");
~ZimFileServer();
@ -95,12 +103,12 @@ private: // data
std::unique_ptr<kiwix::NameMapper> nameMapper;
std::unique_ptr<kiwix::Server> server;
std::unique_ptr<httplib::Client> client;
const Options options = DEFAULT_OPTIONS;
const Cfg cfg;
};
ZimFileServer::ZimFileServer(int serverPort, Options _options, std::string libraryFilePath)
ZimFileServer::ZimFileServer(int serverPort, Cfg _cfg, std::string libraryFilePath)
: manager(&this->library)
, options(_options)
, cfg(_cfg)
{
if ( kiwix::isRelativePath(libraryFilePath) )
libraryFilePath = kiwix::computeAbsolutePath(kiwix::getCurrentDirectory(), libraryFilePath);
@ -109,11 +117,11 @@ ZimFileServer::ZimFileServer(int serverPort, Options _options, std::string libra
}
ZimFileServer::ZimFileServer(int serverPort,
Options _options,
Cfg _cfg,
const FilePathCollection& zimpaths,
std::string indexTemplateString)
: manager(&this->library)
, options(_options)
, cfg(_cfg)
{
for ( const auto& zimpath : zimpaths ) {
if (!manager.addBookFromPath(zimpath, zimpath, "", false))
@ -125,19 +133,19 @@ ZimFileServer::ZimFileServer(int serverPort,
void ZimFileServer::run(int serverPort, std::string indexTemplateString)
{
const std::string address = "127.0.0.1";
if (options & NO_NAME_MAPPER) {
if (cfg.options & NO_NAME_MAPPER) {
nameMapper.reset(new kiwix::IdNameMapper());
} else {
nameMapper.reset(new kiwix::HumanReadableNameMapper(library, false));
}
server.reset(new kiwix::Server(&library, nameMapper.get()));
server->setRoot("ROOT#?");
server->setRoot(cfg.root);
server->setAddress(address);
server->setPort(serverPort);
server->setNbThreads(2);
server->setVerbose(false);
server->setTaskbar(options & WITH_TASKBAR, options & WITH_LIBRARY_BUTTON);
server->setBlockExternalLinks(options & BLOCK_EXTERNAL_LINKS);
server->setTaskbar(cfg.options & WITH_TASKBAR, cfg.options & WITH_LIBRARY_BUTTON);
server->setBlockExternalLinks(cfg.options & BLOCK_EXTERNAL_LINKS);
server->setMultiZimSearchLimit(3);
if (!indexTemplateString.empty()) {
server->setIndexTemplateString(indexTemplateString);
@ -171,9 +179,9 @@ protected:
resetServer(ZimFileServer::DEFAULT_OPTIONS);
}
void resetServer(ZimFileServer::Options options) {
void resetServer(ZimFileServer::Cfg cfg) {
zfs1_.reset();
zfs1_.reset(new ZimFileServer(SERVER_PORT, options, ZIMFILES));
zfs1_.reset(new ZimFileServer(SERVER_PORT, cfg, ZIMFILES));
}
void TearDown() override {