Compare commits

..

1 Commits

Author SHA1 Message Date
Veloman Yunkan
f8fc60ed2f Enabled smart mode of suggestions 2025-05-30 12:33:05 +04:00
96 changed files with 374 additions and 1487 deletions

View File

@@ -11,25 +11,23 @@ jobs:
strategy:
fail-fast: false
matrix:
os:
- macos-13
target:
- macos-aarch64-dyn
- macos-x86_64-dyn
- ios-arm64-dyn
- ios-x86_64-dyn
include:
- target: macos-aarch64-dyn
arch_name: arm64-apple-macos
run_test: true
- target: macos-x86_64-dyn
arch_name: x86_64-apple-darwin
run_test: true
- target: ios-arm64-dyn
arch_name: aarch64-apple-ios
run_test: false
run_test: true
- target: ios-x86_64-dyn
arch_name: x86-apple-ios-simulator
run_test: false
runs-on: macos-15
run_test: true
runs-on: ${{ matrix.os }}
env:
HOME: /Users/runner
@@ -123,24 +121,27 @@ jobs:
- linux-x86_64-dyn
- android-arm
- android-arm64
image_variant: ['jammy']
include:
- target: linux-x86_64-static
image_variant: focal
lib_postfix: '/x86_64-linux-gnu'
arch_name: linux-x86_64
run_test: true
coverage: true
- target: linux-x86_64-dyn
image_variant: focal
lib_postfix: '/x86_64-linux-gnu'
arch_name: linux-x86_64
run_test: true
coverage: true
- target: android-arm
image_variant: focal
lib_postfix: '/arm-linux-androideabi'
arch_name: arm-linux-androideabi
run_test: false
coverage: false
- target: android-arm64
image_variant: focal
lib_postfix: '/aarch64-linux-android'
arch_name: aarch64-linux-android
run_test: false
@@ -149,7 +150,7 @@ jobs:
HOME: /home/runner
runs-on: ubuntu-22.04
container:
image: "ghcr.io/kiwix/kiwix-build_ci_${{matrix.image_variant}}:2025-06-07"
image: "ghcr.io/kiwix/kiwix-build_ci_${{matrix.image_variant}}:38"
steps:
- name: Checkout code
uses: actions/checkout@v4

View File

@@ -21,6 +21,7 @@ jobs:
# - debian-bullseye
- ubuntu-noble
- ubuntu-jammy
- ubuntu-focal
steps:
- uses: actions/checkout@v4
@@ -88,6 +89,14 @@ jobs:
args: --no-sign
ppa: ${{ steps.ppa.outputs.ppa }}
- uses: legoktm/gh-action-build-deb@ubuntu-focal
if: matrix.distro == 'ubuntu-focal'
name: Build package for ubuntu-focal
id: build-ubuntu-focal
with:
args: --no-sign
ppa: ${{ steps.ppa.outputs.ppa }}
- uses: actions/upload-artifact@v4
with:
name: Packages for ${{ matrix.distro }}

View File

@@ -7,7 +7,7 @@ GNU/Linux, macOS, Android, iOS, ...).
[![Release](https://img.shields.io/github/v/tag/kiwix/libkiwix?label=release&sort=semver)](https://download.kiwix.org/release/libkiwix/)
[![Repositories](https://img.shields.io/repology/repositories/libkiwix?label=repositories)](https://github.com/kiwix/libkiwix/wiki/Repology)
[![Build Status](https://github.com/kiwix/libkiwix/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/kiwix/libkiwix/actions?query=branch%3Amain)
[![Build Status](https://github.com/kiwix/libkiwix/workflows/CI/badge.svg?query=branch%3Amain)](https://github.com/kiwix/libkiwix/actions?query=branch%3Amain)
[![Doc](https://readthedocs.org/projects/libkiwix/badge/?style=flat)](https://libkiwix.readthedocs.org/en/latest/?badge=latest)
[![CodeFactor](https://www.codefactor.io/repository/github/kiwix/libkiwix/badge)](https://www.codefactor.io/repository/github/kiwix/libkiwix)
[![Codecov](https://codecov.io/gh/kiwix/libkiwix/branch/main/graph/badge.svg)](https://codecov.io/gh/kiwix/libkiwix)

View File

@@ -7,7 +7,6 @@ headers = [
'downloader.h',
'search_renderer.h',
'server.h',
'spelling_correction.h',
'kiwixserve.h',
'name_mapper.h',
'tools.h',

View File

@@ -50,7 +50,7 @@ class HumanReadableNameMapper : public NameMapper {
std::map<std::string, std::string> m_nameToId;
public:
HumanReadableNameMapper(const kiwix::Library& library, bool withAlias);
HumanReadableNameMapper(kiwix::Library& library, bool withAlias);
virtual ~HumanReadableNameMapper() = default;
virtual std::string getNameForId(const std::string& id) const;
virtual std::string getIdForName(const std::string& name) const;

View File

@@ -63,8 +63,6 @@ namespace kiwix
{ m_withTaskbar = withTaskbar; m_withLibraryButton = withLibraryButton; }
void setBlockExternalLinks(bool blockExternalLinks)
{ m_blockExternalLinks = blockExternalLinks; }
void setCatalogOnlyMode(bool enable) { m_catalogOnlyMode = enable; }
void setContentServerUrl(std::string url) { m_contentServerUrl = url; }
void setIpMode(IpMode mode) { m_ipMode = mode; }
int getPort() const;
IpAddress getAddress() const;
@@ -85,8 +83,6 @@ namespace kiwix
bool m_blockExternalLinks = false;
IpMode m_ipMode = IpMode::AUTO;
int m_ipConnectionLimit = 0;
bool m_catalogOnlyMode = false;
std::string m_contentServerUrl;
std::unique_ptr<InternalServer> mp_server;
};
}

View File

@@ -1,58 +0,0 @@
/*
* Copyright (C) 2025 Veloman Yunkan
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
#ifndef KIWIX_SPELLING_CORRECTION_H
#define KIWIX_SPELLING_CORRECTION_H
#include <filesystem>
#include <memory>
#include <string>
#include <vector>
namespace zim
{
class Archive;
}
namespace Xapian
{
class Database;
}
namespace kiwix
{
class SpellingsDB
{
public: // functions
SpellingsDB(const zim::Archive& archive, std::filesystem::path cacheDirPath);
~SpellingsDB();
SpellingsDB(const SpellingsDB& ) = delete;
void operator=(const SpellingsDB& ) = delete;
std::vector<std::string> getSpellingCorrections(const std::string& word, uint32_t maxCount) const;
private: // data
std::unique_ptr<Xapian::Database> impl_;
};
} // namespace kiwix
#endif // KIWIX_SPELLING_CORRECTION_H

View File

@@ -48,14 +48,7 @@ if (host_machine.system() == 'linux' and compiler.get_id() == 'gcc') or \
else
thread_dep = dependency('', required:false)
endif
libicu_dep = dependency('icu-i18n', static:static_deps)
if libicu_dep.version().version_compare('>= 76')
libicu_deps = [libicu_dep, dependency('icu-uc', static:static_deps)]
else
libicu_deps = [libicu_dep]
endif
pugixml_dep = dependency('pugixml', static:static_deps)
libcurl_dep = dependency('libcurl', static:static_deps)
microhttpd_dep = dependency('libmicrohttpd', static:static_deps)
@@ -70,7 +63,7 @@ else
error('Cannot found header mustache.hpp')
endif
libzim_dep = dependency('libzim', version:['>=9.4.0', '<10.0.0'], static:static_deps)
libzim_dep = dependency('libzim', version:['>=9.0.0', '<10.0.0'], static:static_deps)
if not compiler.has_header_symbol('zim/zim.h', 'LIBZIM_WITH_XAPIAN', dependencies: libzim_dep)
error('Libzim seems to be compiled without Xapian. Xapian support is mandatory.')
@@ -93,11 +86,7 @@ if build_machine.system() == 'windows'
endif
# Dependencies as string
all_deps = [thread_dep, libzim_dep, pugixml_dep, libcurl_dep, microhttpd_dep, zlib_dep, xapian_dep]
# Dependencies as array
all_deps += libicu_deps
all_deps = [thread_dep, libicu_dep, libzim_dep, pugixml_dep, libcurl_dep, microhttpd_dep, zlib_dep, xapian_dep]
inc = include_directories('include', extra_include)

View File

@@ -82,11 +82,10 @@ void Book::update(const zim::Archive& archive) {
m_size = static_cast<uint64_t>(getArchiveFileSize(archive)) << 10;
m_illustrations.clear();
for ( const auto& illustrationInfo : archive.getIllustrationInfos() ) {
for ( const auto illustrationSize : archive.getIllustrationSizes() ) {
const auto illustration = std::make_shared<Illustration>();
const zim::Item illustrationItem = archive.getIllustrationItem(illustrationInfo);
illustration->width = illustrationInfo.width;
illustration->height = illustrationInfo.height;
const zim::Item illustrationItem = archive.getIllustrationItem(illustrationSize);
illustration->width = illustration->height = illustrationSize;
illustration->mimeType = illustrationItem.getMimetype();
illustration->data = illustrationItem.getData();
// NOTE: illustration->url is left uninitialized

View File

@@ -30,7 +30,7 @@ std::string humanFriendlyTitle(std::string title)
kainjow::mustache::object getLangTag(const std::vector<std::string>& bookLanguages) {
std::string langShortString = "";
std::string langFullString = "???";
//if more than 1 languages then show "mul" else show the language
if(bookLanguages.size() > 1) {
std::vector<std::string> mulLanguages;
@@ -44,7 +44,7 @@ kainjow::mustache::object getLangTag(const std::vector<std::string>& bookLanguag
langShortString = bookLanguages[0];
langFullString = getLanguageSelfName(langShortString);
}
kainjow::mustache::object langTag;
langTag["langShortString"] = langShortString;
langTag["langFullString"] = langFullString;
@@ -130,7 +130,6 @@ std::string HTMLDumper::dumpPlainHTML(kiwix::Filter filter) const
RESOURCE::templates::no_js_library_page_html,
kainjow::mustache::object{
{"root", rootLocation},
{"contentAccessUrl", onlyAsNonEmptyMustacheValue(contentAccessUrl)},
{"books", booksData },
{"searchQuery", searchQuery},
{"languages", languages},

View File

@@ -50,13 +50,6 @@ class LibraryDumper
*/
void setRootLocation(const std::string& rootLocation) { this->rootLocation = rootLocation; }
/**
* Set the URL for accessing book content
*
* @param url the URL of the /content endpoint of the content server
*/
void setContentAccessUrl(const std::string& url) { this->contentAccessUrl = url; }
/**
* Set some informations about the search results.
*
@@ -65,10 +58,10 @@ class LibraryDumper
* @param count the number of result of the current set (or page).
*/
void setOpenSearchInfo(int totalResult, int startIndex, int count);
/**
* Sets user default language
*
*
* @param userLang the user language to be set
*/
void setUserLanguage(std::string userLang) { this->m_userLang = userLang; }
@@ -88,7 +81,6 @@ class LibraryDumper
const kiwix::NameMapper* const nameMapper;
std::string libraryId;
std::string rootLocation;
std::string contentAccessUrl;
std::string m_userLang;
int m_totalResults;
int m_startIndex;

View File

@@ -31,7 +31,6 @@ kiwix_sources = [
'server/internalServer_catalog.cpp',
'server/i18n.cpp',
'opds_catalog.cpp',
'spelling_correction.cpp',
'version.cpp'
]
kiwix_sources += lib_resources

View File

@@ -24,8 +24,8 @@
namespace kiwix {
HumanReadableNameMapper::HumanReadableNameMapper(const kiwix::Library& library, bool withAlias) {
for (auto& bookId: library.filter(kiwix::Filter())) {
HumanReadableNameMapper::HumanReadableNameMapper(kiwix::Library& library, bool withAlias) {
for (auto& bookId: library.filter(kiwix::Filter().local(true).valid(true))) {
auto& currentBook = library.getBookById(bookId);
auto bookName = currentBook.getHumanReadableIdFromPath();
m_idToName[bookId] = bookName;

View File

@@ -51,26 +51,24 @@ typedef kainjow::mustache::list IllustrationInfo;
IllustrationInfo getBookIllustrationInfo(const Book& book)
{
kainjow::mustache::list illustrations;
for ( const auto& illustration : book.getIllustrations() ) {
// For now, we are handling only sizexsize@1 illustration.
// So we can simply pass one size to mustache.
illustrations.push_back(kainjow::mustache::object{
{"icon_size", to_string(illustration->width)},
{"icon_mimetype", illustration->mimeType}
});
if ( book.isPathValid() ) {
for ( const auto& illustration : book.getIllustrations() ) {
// For now, we are handling only sizexsize@1 illustration.
// So we can simply pass one size to mustache.
illustrations.push_back(kainjow::mustache::object{
{"icon_size", to_string(illustration->width)},
{"icon_mimetype", illustration->mimeType}
});
}
}
return illustrations;
}
std::string fullEntryXML(const Book& book,
const std::string& rootLocation,
const std::string& contentAccessUrl,
const std::string& contentId)
std::string fullEntryXML(const Book& book, const std::string& rootLocation, const std::string& contentId)
{
const auto bookDate = book.getDate() + "T00:00:00Z";
const kainjow::mustache::object data{
{"root", rootLocation},
{"contentAccessUrl", onlyAsNonEmptyMustacheValue(contentAccessUrl)},
{"id", book.getId()},
{"name", book.getName()},
{"title", book.getTitle()},
@@ -107,12 +105,7 @@ std::string partialEntryXML(const Book& book, const std::string& rootLocation)
return render_template(xmlTemplate, data);
}
BooksData getBooksData(const Library* library,
const NameMapper* nameMapper,
const std::vector<std::string>& bookIds,
const std::string& rootLocation,
const std::string& contentAccessUrl,
bool partial)
BooksData getBooksData(const Library* library, const NameMapper* nameMapper, const std::vector<std::string>& bookIds, const std::string& rootLocation, bool partial)
{
BooksData booksData;
for ( const auto& bookId : bookIds ) {
@@ -121,7 +114,7 @@ BooksData getBooksData(const Library* library,
const std::string contentId = nameMapper->getNameForId(bookId);
const auto entryXML = partial
? partialEntryXML(book, rootLocation)
: fullEntryXML(book, rootLocation, contentAccessUrl, contentId);
: fullEntryXML(book, rootLocation, contentId);
booksData.push_back(kainjow::mustache::object{ {"entry", entryXML} });
} catch ( const std::out_of_range& ) {
// the book was removed from the library since its id was obtained
@@ -136,7 +129,7 @@ BooksData getBooksData(const Library* library,
string OPDSDumper::dumpOPDSFeed(const std::vector<std::string>& bookIds, const std::string& query) const
{
const auto booksData = getBooksData(library, nameMapper, bookIds, rootLocation, contentAccessUrl, false);
const auto booksData = getBooksData(library, nameMapper, bookIds, rootLocation, false);
const kainjow::mustache::object template_data{
{"date", gen_date_str()},
{"root", rootLocation},
@@ -154,7 +147,7 @@ string OPDSDumper::dumpOPDSFeed(const std::vector<std::string>& bookIds, const s
string OPDSDumper::dumpOPDSFeedV2(const std::vector<std::string>& bookIds, const std::string& query, bool partial) const
{
const auto endpointRoot = rootLocation + "/catalog/v2";
const auto booksData = getBooksData(library, nameMapper, bookIds, rootLocation, contentAccessUrl, partial);
const auto booksData = getBooksData(library, nameMapper, bookIds, rootLocation, partial);
const char* const endpoint = partial ? "/partial_entries" : "/entries";
const std::string url = endpoint + (query.empty() ? "" : "?" + query);
@@ -179,7 +172,7 @@ std::string OPDSDumper::dumpOPDSCompleteEntry(const std::string& bookId) const
const std::string contentId = nameMapper->getNameForId(bookId);
return XML_HEADER
+ "\n"
+ fullEntryXML(book, rootLocation, contentAccessUrl, contentId);
+ fullEntryXML(book, rootLocation, contentId);
}
std::string OPDSDumper::categoriesOPDSFeed() const

View File

@@ -58,7 +58,7 @@ ParameterizedMessage searchResultsPageHeaderMsg(const std::string& searchPattern
return ParameterizedMessage("search-results-page-header",
{
{"SEARCH_PATTERN", searchPattern},
{"START", r.get("startLabel")->string_value()},
{"START", r.get("start")->string_value()},
{"END", r.get("end") ->string_value()},
{"COUNT", r.get("count")->string_value()},
}
@@ -225,8 +225,7 @@ std::string SearchRenderer::renderTemplate(const std::string& tmpl_str, const Na
results.set("items", items);
results.set("count", kiwix::beautifyInteger(estimatedResultCount));
results.set("start", kiwix::beautifyInteger(resultStart));
results.set("startLabel", kiwix::beautifyInteger(resultStart+1));
results.set("end", kiwix::beautifyInteger(std::min(resultStart+pageLength, estimatedResultCount)));
results.set("end", kiwix::beautifyInteger(std::min(resultStart+pageLength-1, estimatedResultCount)));
// pagination
auto pagination = buildPagination(

View File

@@ -53,9 +53,7 @@ bool Server::start() {
m_blockExternalLinks,
m_ipMode,
m_indexTemplateString,
m_ipConnectionLimit,
m_catalogOnlyMode,
m_contentServerUrl));
m_ipConnectionLimit));
return mp_server->start();
}

View File

@@ -125,12 +125,9 @@ std::string getSearchComponent(const RequestContext& request)
return query.empty() ? query : "?" + query;
}
Filter get_search_filter(const RequestContext& request, const std::string& prefix="", bool catalogOnlyMode = false)
Filter get_search_filter(const RequestContext& request, const std::string& prefix="")
{
auto filter = kiwix::Filter();
if ( !catalogOnlyMode ) {
filter.valid(true).local(true);
}
auto filter = kiwix::Filter().valid(true).local(true);
try {
filter.query(request.get_argument(prefix+"q"));
} catch (const std::out_of_range&) {}
@@ -435,9 +432,7 @@ InternalServer::InternalServer(LibraryPtr library,
bool blockExternalLinks,
IpMode ipMode,
std::string indexTemplateString,
int ipConnectionLimit,
bool catalogOnlyMode,
std::string contentServerUrl) :
int ipConnectionLimit) :
m_addr(addr),
m_port(port),
m_root(normalizeRootUrl(root)),
@@ -456,9 +451,7 @@ InternalServer::InternalServer(LibraryPtr library,
mp_nameMapper(nameMapper ? nameMapper : std::shared_ptr<NameMapper>(&defaultNameMapper, NoDelete())),
searchCache(getEnvVar<int>("KIWIX_SEARCH_CACHE_SIZE", DEFAULT_CACHE_SIZE)),
suggestionSearcherCache(getEnvVar<int>("KIWIX_SUGGESTION_SEARCHER_CACHE_SIZE", std::max((unsigned int) (mp_library->getBookCount(true, true)*0.1), 1U))),
m_customizedResources(new CustomizedResources),
m_catalogOnlyMode(catalogOnlyMode),
m_contentServerUrl(contentServerUrl)
m_customizedResources(new CustomizedResources)
{
m_root = urlEncode(m_root);
}
@@ -716,10 +709,12 @@ std::unique_ptr<Response> InternalServer::handle_request(const RequestContext& r
return Response::build_redirect(contentUrl + query);
} catch (std::exception& e) {
fprintf(stderr, "===== Unhandled error : %s\n", e.what());
return HTTP500Response(request, m_root, request.get_full_url(), e.what());
return HTTP500Response(request)
+ ParameterizedMessage("non-translated-text", {{"MSG", e.what()}});
} catch (...) {
fprintf(stderr, "===== Unhandled unknown error\n");
return HTTP500Response(request, m_root, request.get_full_url());
return HTTP500Response(request)
+ nonParameterizedMessage("unknown-error");
}
}
@@ -780,6 +775,7 @@ std::unique_ptr<Response> InternalServer::handle_suggest(const RequestContext& r
}
const auto queryString = request.get_optional_param("term", std::string());
const auto mode = request.get_optional_param("mode", std::string());
const auto start = request.get_optional_param<unsigned int>("start", 0);
unsigned int count = request.get_optional_param<unsigned int>("count", 10);
if (count == 0) {
@@ -798,13 +794,17 @@ std::unique_ptr<Response> InternalServer::handle_suggest(const RequestContext& r
);
const auto lock(searcher->getLock());
auto search = searcher->suggest(queryString);
auto srs = search.getResults(start, count);
for(auto& suggestion: srs) {
results.add(suggestion);
if ( start == 0 && mode == "smart") {
for(const auto& suggestion: search.getSmartSuggestions(count)) {
results.add(suggestion);
}
} else {
for(const auto& suggestion: search.getResults(start, count)) {
results.add(suggestion);
}
}
/* Propose the fulltext search if possible */
if (archive->hasFulltextIndex()) {
results.addFTSearchSuggestion(request.get_user_language(), queryString);
@@ -850,15 +850,6 @@ std::string InternalServer::getNoJSDownloadPageHTML(const std::string& bookId, c
);
}
void InternalServer::setContentAccessUrl(LibraryDumper& libDumper) const
{
if ( !m_contentServerUrl.empty() ) {
libDumper.setContentAccessUrl(m_contentServerUrl + "/content");
} else if ( !m_catalogOnlyMode ) {
libDumper.setContentAccessUrl(m_root + "/content");
}
}
std::unique_ptr<Response> InternalServer::handle_no_js(const RequestContext& request)
{
const auto url = request.get_url();
@@ -866,13 +857,12 @@ std::unique_ptr<Response> InternalServer::handle_no_js(const RequestContext& req
HTMLDumper htmlDumper(mp_library.get(), mp_nameMapper.get());
htmlDumper.setRootLocation(m_root);
htmlDumper.setLibraryId(getLibraryId());
setContentAccessUrl(htmlDumper);
auto userLang = request.get_user_language();
htmlDumper.setUserLanguage(userLang);
std::string content;
if (urlParts.size() == 1) {
auto filter = get_search_filter(request, "", m_catalogOnlyMode);
auto filter = get_search_filter(request);
try {
if (request.get_argument("category") == "") {
filter.clearCategory();
@@ -1021,11 +1011,11 @@ std::unique_ptr<Response> InternalServer::handle_search_request(const RequestCon
return response;
}
const auto start = max(0u, request.get_optional_param("start", 0u));
const auto start = max(1u, request.get_optional_param("start", 1u));
const auto pageLength = getSearchPageSize(request);
/* Get the results */
SearchRenderer renderer(search->getResults(start, pageLength), start,
SearchRenderer renderer(search->getResults(start-1, pageLength), start,
search->getEstimatedMatches());
renderer.setSearchPattern(searchInfo.pattern);
renderer.setSearchBookQuery(searchInfo.bookFilterQuery);
@@ -1120,7 +1110,7 @@ std::vector<std::string>
InternalServer::search_catalog(const RequestContext& request,
kiwix::OPDSDumper& opdsDumper)
{
const auto filter = get_search_filter(request, "", m_catalogOnlyMode);
const auto filter = get_search_filter(request);
std::vector<std::string> bookIdsToDump = mp_library->filter(filter);
const auto totalResults = bookIdsToDump.size();
const long count = request.get_optional_param("count", 10L);
@@ -1151,7 +1141,6 @@ const std::string CONTENT_CSP_HEADER =
"allow-same-origin "
"allow-modals "
"allow-popups "
"allow-popups-to-escape-sandbox "
"allow-forms "
"allow-downloads;";

View File

@@ -90,7 +90,6 @@ class SearchInfo {
typedef kainjow::mustache::data MustacheData;
class OPDSDumper;
class LibraryDumper;
class InternalServer {
public:
@@ -107,9 +106,7 @@ class InternalServer {
bool blockExternalLinks,
IpMode ipMode,
std::string indexTemplateString,
int ipConnectionLimit,
bool catalogOnlyMode,
std::string zimViewerURL);
int ipConnectionLimit);
virtual ~InternalServer();
MHD_Result handlerCallback(struct MHD_Connection* connection,
@@ -163,8 +160,6 @@ class InternalServer {
std::string getLibraryId() const;
std::string getNoJSDownloadPageHTML(const std::string& bookId, const std::string& userLang) const;
OPDSDumper getOPDSDumper() const;
void setContentAccessUrl(LibraryDumper& libDumper) const;
private: // types
class LockableSuggestionSearcher;
@@ -197,9 +192,6 @@ class InternalServer {
class CustomizedResources;
std::unique_ptr<CustomizedResources> m_customizedResources;
const bool m_catalogOnlyMode;
const std::string m_contentServerUrl;
};
}

View File

@@ -51,15 +51,6 @@ const std::string opdsMimeType[] = {
} // unnamed namespace
OPDSDumper InternalServer::getOPDSDumper() const
{
kiwix::OPDSDumper opdsDumper(mp_library.get(), mp_nameMapper.get());
opdsDumper.setRootLocation(m_root);
opdsDumper.setLibraryId(getLibraryId());
setContentAccessUrl(opdsDumper);
return opdsDumper;
}
std::unique_ptr<Response> InternalServer::handle_catalog(const RequestContext& request)
{
if (m_verbose.load()) {
@@ -89,7 +80,9 @@ std::unique_ptr<Response> InternalServer::handle_catalog(const RequestContext& r
}
zim::Uuid uuid;
kiwix::OPDSDumper opdsDumper = getOPDSDumper();
kiwix::OPDSDumper opdsDumper(mp_library.get(), mp_nameMapper.get());
opdsDumper.setRootLocation(m_root);
opdsDumper.setLibraryId(getLibraryId());
std::vector<std::string> bookIdsToDump;
if (url == "root.xml") {
uuid = zim::Uuid::generate(host);
@@ -165,7 +158,9 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_root(const RequestCo
std::unique_ptr<Response> InternalServer::handle_catalog_v2_entries(const RequestContext& request, bool partial)
{
kiwix::OPDSDumper opdsDumper = getOPDSDumper();
OPDSDumper opdsDumper(mp_library.get(), mp_nameMapper.get());
opdsDumper.setRootLocation(m_root);
opdsDumper.setLibraryId(getLibraryId());
const auto bookIds = search_catalog(request, opdsDumper);
const auto opdsFeed = opdsDumper.dumpOPDSFeedV2(bookIds, request.get_query(), partial);
return ContentResponse::build(
@@ -182,7 +177,9 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_complete_entry(const
return UrlNotFoundResponse(request);
}
kiwix::OPDSDumper opdsDumper = getOPDSDumper();
OPDSDumper opdsDumper(mp_library.get(), mp_nameMapper.get());
opdsDumper.setRootLocation(m_root);
opdsDumper.setLibraryId(getLibraryId());
const auto opdsFeed = opdsDumper.dumpOPDSCompleteEntry(entryId);
return ContentResponse::build(
opdsFeed,
@@ -192,7 +189,9 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_complete_entry(const
std::unique_ptr<Response> InternalServer::handle_catalog_v2_categories(const RequestContext& request)
{
kiwix::OPDSDumper opdsDumper = getOPDSDumper();
OPDSDumper opdsDumper(mp_library.get(), mp_nameMapper.get());
opdsDumper.setRootLocation(m_root);
opdsDumper.setLibraryId(getLibraryId());
return ContentResponse::build(
opdsDumper.categoriesOPDSFeed(),
opdsMimeType[OPDS_NAVIGATION_FEED]
@@ -201,7 +200,9 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_categories(const Req
std::unique_ptr<Response> InternalServer::handle_catalog_v2_languages(const RequestContext& request)
{
kiwix::OPDSDumper opdsDumper = getOPDSDumper();
OPDSDumper opdsDumper(mp_library.get(), mp_nameMapper.get());
opdsDumper.setRootLocation(m_root);
opdsDumper.setLibraryId(getLibraryId());
return ContentResponse::build(
opdsDumper.languagesOPDSFeed(),
opdsMimeType[OPDS_NAVIGATION_FEED]

View File

@@ -491,30 +491,15 @@ HTTP400Response::HTTP400Response(const RequestContext& request)
*this += ParameterizedMessage("invalid-request", {{"url", requestUrl}});
}
HTTP500Response::HTTP500Response(const RequestContext& request,
const std::string& root,
const std::string& urlPath,
const std::string& errorText)
: ContentResponseBlueprint(&request,
MHD_HTTP_INTERNAL_SERVER_ERROR,
"text/html; charset=utf-8",
RESOURCE::templates::sexy500_html,
/*includeKiwixResponseData=*/true)
HTTP500Response::HTTP500Response(const RequestContext& request)
: HTTPErrorResponse(request,
MHD_HTTP_INTERNAL_SERVER_ERROR,
"500-page-title",
"500-page-heading",
std::string(),
/*includeKiwixResponseData=*/true)
{
auto pageParams = Data::Object{
{"root", root },
{"url_path", urlPath},
{"PAGE_TITLE", Data::fromMsgId("500-page-title")},
{"PAGE_HEADING", Data::fromMsgId("500-page-heading")},
{"PAGE_TEXT", Data::fromMsgId("500-page-text")},
{"500_img_text", Data::fromMsgId("500-img-text")},
};
if ( !errorText.empty() ) {
pageParams["error"] = errorText;
}
*this->m_data = Data(pageParams);
*this += nonParameterizedMessage("500-page-text");
}
std::unique_ptr<Response> Response::build_416(size_t resourceLength)

View File

@@ -180,12 +180,9 @@ struct HTTP400Response : HTTPErrorResponse
explicit HTTP400Response(const RequestContext& request);
};
struct HTTP500Response : ContentResponseBlueprint
struct HTTP500Response : HTTPErrorResponse
{
HTTP500Response(const RequestContext& request,
const std::string& root,
const std::string& urlPath,
const std::string& error = "");
explicit HTTP500Response(const RequestContext& request);
};
class ItemResponse : public Response {

View File

@@ -1,108 +0,0 @@
/*
* Copyright (C) 2025 Veloman Yunkan
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
#include "spelling_correction.h"
#include "zim/archive.h"
#include <sstream>
#include <stdexcept>
#include <xapian.h>
namespace kiwix
{
namespace
{
std::vector<std::string> getAllTitles(const zim::Archive& a)
{
std::vector<std::string> result;
for (const auto& entry : a.iterByPath() ) {
result.push_back(entry.getTitle());
}
return result;
}
void createXapianDB(std::string path, const zim::Archive& archive)
{
const int flags = Xapian::DB_BACKEND_GLASS|Xapian::DB_CREATE;
const auto tmpDbPath = path + ".tmp";
Xapian::WritableDatabase db(tmpDbPath, flags);
for (const auto& t : getAllTitles(archive)) {
db.add_spelling(t);
}
db.commit();
db.compact(path, Xapian::DBCOMPACT_SINGLE_FILE);
db.close();
std::filesystem::remove_all(tmpDbPath);
}
std::string spellingsDBPathForZIMArchive(std::filesystem::path cacheDirPath, const zim::Archive& a)
{
// The version of spellings DB must be updated each time an important change
// to the implementation is made that renders using the previous version
// impossible or undesirable.
const char SPELLINGS_DB_VERSION[] = "0.1";
std::ostringstream filename;
filename << a.getUuid() << ".spellingsdb.v" << SPELLINGS_DB_VERSION;
return (cacheDirPath / filename.str()).string();
}
std::unique_ptr<Xapian::Database> openOrCreateXapianDB(std::filesystem::path cacheDirPath, const zim::Archive& archive)
{
const auto path = spellingsDBPathForZIMArchive(cacheDirPath, archive);
try
{
return std::make_unique<Xapian::Database>(path);
}
catch (const Xapian::DatabaseOpeningError& )
{
createXapianDB(path, archive);
return std::make_unique<Xapian::Database>(path);
}
}
} // unnamed namespace
SpellingsDB::SpellingsDB(const zim::Archive& archive, std::filesystem::path cacheDirPath)
: impl_(openOrCreateXapianDB(cacheDirPath, archive))
{
}
SpellingsDB::~SpellingsDB()
{
}
std::vector<std::string> SpellingsDB::getSpellingCorrections(const std::string& word, uint32_t maxCount) const
{
if ( maxCount > 1 ) {
throw std::runtime_error("More than one spelling correction was requested");
}
std::vector<std::string> result;
const auto term = impl_->get_spelling_suggestion(word, 3);
if ( !term.empty() ) {
result.push_back(term);
}
return result;
}
} // namespace kiwix

View File

@@ -384,9 +384,13 @@ void kiwix::Suggestions::add(const zim::SuggestionItem& suggestion)
: suggestion.getTitle();
result.set("label", escapeForJSON(label, DONT_ESCAPE_QUOTE));
result.set("value", escapeForJSON(suggestion.getTitle(), DONT_ESCAPE_QUOTE));
result.set("kind", "path");
result.set("path", escapeForJSON(suggestion.getPath(), DONT_ESCAPE_QUOTE));
if ( suggestion.getPath().empty() ) {
result.set("kind", "modifiedquery");
} else {
result.set("kind", "path");
result.set("value", escapeForJSON(suggestion.getTitle(), DONT_ESCAPE_QUOTE));
result.set("path", escapeForJSON(suggestion.getPath(), DONT_ESCAPE_QUOTE));
}
result.set("first", m_data.is_empty_list());
m_data.push_back(result);
}

View File

@@ -13,7 +13,6 @@ skin/i18n/fr.json
skin/i18n/ha.json
skin/i18n/he.json
skin/i18n/hi.json
skin/i18n/hu.json
skin/i18n/hy.json
skin/i18n/ia.json
skin/i18n/id.json

View File

@@ -2,7 +2,6 @@ skin/caret.png
skin/bittorrent.png
skin/magnet.png
skin/404.svg
skin/500.svg
skin/blocklink.svg
skin/feed.svg
skin/langSelector.svg
@@ -15,7 +14,6 @@ skin/isotope.pkgd.min.js
skin/index.js
skin/autoComplete/autoComplete.min.js
skin/error.css
skin/print.css
skin/kiwix.css
skin/taskbar.css
skin/index.css
@@ -49,7 +47,6 @@ templates/viewer_settings.js
templates/no_js_library_page.html
templates/no_js_download.html
templates/sexy404.html
templates/sexy500.html
opensearchdescription.xml
ft_opensearchdescription.xml
catalog_v2_searchdescription.xml

View File

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 9.7 KiB

View File

@@ -3,12 +3,11 @@
"authors": [
"Asma",
"Hamoudak",
"Meno25",
"Mohammed Qays",
"Ravan",
"محمد أحمد عبد الفتاح"
]
},
"name": "الإنجليزية",
"no-such-book": "لا يوجد مثل هذا الكتاب: {{BOOK_NAME}}",
"too-many-books": "طلب العديد من الكتب {{NB_BOOKS}} حيث الحد {{LIMIT}}",
"no-book-found": "لا يوجد كتاب يطابق معايير الاختيار",
@@ -24,14 +23,13 @@
"404-page-title": "المحتوى غير موجود",
"404-page-heading": "لم يتم العثور عليه",
"500-page-title": "خطأ داخلي بالخادم",
"500-page-heading": "عفواً، الصفحة لا تعمل.",
"500-page-text": "لا يمكن إرسال المسار المطلوب بشكل صحيح:",
"500-page-heading": "خطأ داخلي بالخادم",
"fulltext-search-unavailable": "البحث في كامل النص غير متاح",
"no-search-results": "محرك البحث في كامل النص غير متوفر لهذا المحتوي.",
"library-button-text": "توجه إلي صفحة الترحيب",
"home-button-text": "انتقل إلى الصفحة الرئيسة لـ '{{{BOOK_TITLE}}}'",
"home-button-text": "انتقل إلى الصفحة الرئيسية لـ \"{{BOOK_TITLE}}\"",
"random-page-button-text": "اذهب إلى صفحة عشوائية",
"searchbox-tooltip": "ابحث عن '{{{BOOK_TITLE}}}'",
"searchbox-tooltip": "بحث \"{{BOOK_TITLE}}\"",
"confusion-of-tongues": "قد يشارك في البحث كتابان أو أكثر بلغات مختلفة، مما قد يؤدي إلى نتائج محيرة.",
"direct-download-alt-text": "التنزيل مباشرة عبر بروتوكول HTTP(S)",
"hash-download-link-text": "المجموع الإختباري لخوارزمية SHA-256",

View File

@@ -1,23 +1,13 @@
{
"@metadata": {
"authors": [
"Nokib Sarkar",
"আফতাবুজ্জামান"
]
},
"name": "বাংলা",
"404-page-heading": "পাওয়া যায়নি",
"new-404-page-title": "পাতা পাওয়া যায়নি",
"new-404-page-heading": "ওহ! পাতা খুঁজে পাওয়া যায়নি",
"404-img-text": "পাওয়া যায়নি!",
"path-was-not-found": "অনুরোধ করা পথটি পাওয়া যায়নি:",
"404-advice.p1": "আপনি যে বিষয়বস্তু খুঁজছেন তা এখনও উপলব্ধ থাকতে পারে, তবে এটি জিম ফাইলের মধ্যে অন্য কোনও জায়গায় অবস্থিত হতে পারে।",
"404-advice.p4": "আপনি যে তথ্য খুঁজছেন তার সাথে সম্পর্কিত কীওয়ার্ড বা শিরোনাম খুঁজুন।",
"500-page-title": "অভ্যন্তরীণ সার্ভার ত্রুটি",
"500-page-heading": "অভ্যন্তরীণ সার্ভার ত্রুটি",
"external-link-intro": "আপনি অনলাইনে যাওয়ার জন্য কিউইক্সের জিম রিডার ছেড়ে দিতে চলেছেন",
"external-link-advice.p1": "আপনি যে লিঙ্কটিতে প্রবেশ করার চেষ্টা করছেন তা আপনার অফলাইন প্যাকেজের অংশ নয় এবং এর জন্য ইন্টারনেট সংযোগ প্রয়োজন।",
"external-link-advice.p2": "আপনি যদি অনলাইনে যেতে পারেন, তাহলে লিঙ্কটি খোলার চেষ্টা করতে পারেন।",
"external-link-advice.p3": "অন্যথায়, আপনি আপনার ব্রাউজারের পিছন বোতাম ব্যবহার করে আপনার ZIM-এর অফলাইন বিষয়বস্তুতে ফিরে যেতে পারেন।",
"search-result-book-info": "{{BOOK_TITLE}} থেকে",
"word-count": "{{COUNT}}টি শব্দ",
"library-button-text": "স্বাগত পাতায় চলুন",

View File

@@ -5,6 +5,7 @@
"Y-M D"
]
},
"name": "brezhoneg",
"suggest-full-text-search": "E lec'h emañ \"{{{SEARCH_TERMS}}}\"...",
"no-such-book": "Neus ket eus al levr-mañ: {{BOOK_NAME}}",
"no-book-found": "Neus levr ebet a glot gant an dezverkoù-se",

View File

@@ -4,6 +4,7 @@
"Spotter"
]
},
"name": "Čeština",
"suggest-full-text-search": "obsahující '{{{SEARCH_TERMS}}}'...",
"no-such-book": "Žádná taková kniha: {{BOOK_NAME}}",
"too-many-books": "Bylo požadováno příliš mnoho knih ({{NB_BOOKS}}), kde je limit {{LIMIT}}",

View File

@@ -5,6 +5,7 @@
"Ruky Wunpini"
]
},
"name": "Silimiinsili",
"suggest-full-text-search": "Gbubi la '{{{SEARCH_TERMS}}}'...",
"no-such-book": "Lala buku kani:{{BOOK_NAME}}",
"too-many-books": "Buku nima pam ka bɛ daa suhi ({{NB_BOOKS}}) din ni ka tariga nyɛ {{LIMIT}}",

View File

@@ -5,11 +5,11 @@
"IMayBeABitShy",
"Justman10000",
"Lucas Werkmeister",
"MoritzMT20",
"Rofiatmustapha12",
"ThisCarthing"
]
},
"name": "Deutsch",
"suggest-full-text-search": "enthält '{{{SEARCH_TERMS}}}'...",
"no-such-book": "Buch nicht gefunden: {{BOOK_NAME}}",
"too-many-books": "Zu viele Bücher angefragt ({{NB_BOOKS}}), die Beschränkung liegt bei {{LIMIT}}",
@@ -37,9 +37,9 @@
"search-result-book-info": "von {{BOOK_TITLE}}",
"word-count": "{{COUNT}} Wörter",
"library-button-text": "Zur Willkommensseite gehen",
"home-button-text": "Zur Hauptseite von '{{{BOOK_TITLE}}}' gehen",
"home-button-text": "Zur Hauptseite von '{{BOOK_TITLE}}' gehen",
"random-page-button-text": "Zu einer zufällig ausgewählten Seite gehen",
"searchbox-tooltip": "'{{{BOOK_TITLE}}}' durchsuchen",
"searchbox-tooltip": "Nach '{{BOOK_TITLE}}' suchen",
"confusion-of-tongues": "Zwei oder mehr Bücher unterschiedlicher Sprachen werden durchsucht, was zu unübersichtlichen Ergebnissen führen kann.",
"welcome-page-overzealous-filter": "Keine Ergebnisse gefunden. Möchten Sie den <a href=\"{{URL}}\">Filter zurücksetzen</a>?",
"powered-by-kiwix-html": "Angetrieben durch &nbsp;<a href=\"https://kiwix.org\">Kiwix</a>",

View File

@@ -6,6 +6,7 @@
"Ανώνυμος Βικιπαιδιστής"
]
},
"name": "Αγγλικά",
"no-such-book": "Δεν υπάρχει τέτοιο βιβλίο: {{BOOK_NAME}}",
"welcome-page-overzealous-filter": "Κανένα αποτέλεσμα. Θέλετε να <a href=\"{{URL}}\">επαναφέρετε το φίλτρο</a>;",
"powered-by-kiwix-html": "Με την υποστήριξη by&nbsp;<a href=\"https://kiwix.org\">Kiwix</a>",

View File

@@ -3,6 +3,7 @@
"authors": [
]
},
"name":"English",
"suggest-full-text-search" : "containing '{{{SEARCH_TERMS}}}'..."
, "no-such-book" : "No such book: {{BOOK_NAME}}"
, "too-many-books" : "Too many books requested ({{NB_BOOKS}}) where limit is {{LIMIT}}"
@@ -29,9 +30,8 @@
, "404-advice.p4": "Look for keywords or titles related to the information you're seeking"
, "404-advice.p5": "This approach should help you locate the desired content, even if the original link isn't working properly."
, "500-page-title" : "Internal Server Error"
, "500-page-heading" : "Oops. Page isn't working."
, "500-page-text": "The requested path cannot be properly delivered:"
, "500-img-text": "Page isn't working"
, "500-page-heading" : "Internal Server Error"
, "500-page-text": "An internal server error occured. We are sorry about that :/"
, "external-link-detected" : "External Link Detected"
, "caution-warning" : "Caution!"
, "external-link-intro" : "You are about to leave Kiwix's ZIM reader to go online to"
@@ -46,9 +46,9 @@
, "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}}}'"
, "home-button-text": "Go to the main page of '{{BOOK_TITLE}}'"
, "random-page-button-text": "Go to a randomly selected page"
, "searchbox-tooltip": "Search '{{{BOOK_TITLE}}}'"
, "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=\"{{URL}}\">reset filter</a>?"
, "powered-by-kiwix-html": "Powered by&nbsp;<a href=\"https://kiwix.org\">Kiwix</a>"

View File

@@ -3,17 +3,17 @@
"authors": [
"AlexanderFF",
"Fitoschido",
"Laranxa",
"Ovruni",
"Sinopsistrans",
"SpikeShroom",
"Vis4valentine"
]
},
"name": "español",
"suggest-full-text-search": "que contenga \"{{{SEARCH_TERMS}}}\"...",
"no-such-book": "No existe el libro: {{BOOK_NAME}}",
"too-many-books": "Demasiadas solicitudes de libros ({{NB_BOOKS}}) donde el límite es {{LIMIT}}",
"no-book-found": "Ningún libro coincide con los criterios de selección",
"no-book-found": "Ningún libro coincide con los criterios de selección.",
"url-not-found": "La URL solicitada \"{{url}}\" no se encontró en este servidor.",
"suggest-search": "Haga una búsqueda de texto completo para <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
"random-article-failure": "¡Ups! Error al elegir un artículo aleatorio :(",
@@ -26,25 +26,9 @@
"400-page-heading": "Solicitud inválida",
"404-page-title": "Contenido no encontrado",
"404-page-heading": "No encontrado",
"new-404-page-title": "Página no encontrada",
"new-404-page-heading": "Ups. Página no encontrado.",
"404-img-text": "¡No encontrado!",
"path-was-not-found": "La pagina solicitada no se ha encontrado:",
"404-advice.p1": "El contenido que está buscando podría estar disponible pero podría estar localizado en un lugar diferente dentro del archivo ZIM.",
"404-advice.p2": "Por favor:",
"404-advice.p3": "Intente usar la función de búsqueda para encontrar el contenido que desea",
"404-advice.p4": "Mire por palabras clave o títulos relacionados a la información que está buscando",
"404-advice.p5": "Este método debería ayudarle a localizar el contenido deseado incluso si el enlace original no funciona correctamente.",
"500-page-title": "Error interno del servidor",
"500-page-heading": "Ups. La página no funciona.",
"500-page-text": "La pagina solicitada no se puede cargar correctamente:",
"500-img-text": "La página no está funcionando",
"external-link-detected": "Enlace externo detectado",
"caution-warning": "¡Precaución!",
"external-link-intro": "Usted está apunto de abandonar el lector ZIM de Kiwix para ir a",
"external-link-advice.p1": "El enlace al que está tratando de acceder no es parte de su descarga sin conexión y requiere de conexión a internet.",
"external-link-advice.p2": "Si usted puede conectarse en linea puede intentar abrir el enlace.",
"external-link-advice.p3": "De lo contrario, puede volver al contenido sin conexión de su ZIM utilizando el botón atrás del navegador.",
"500-page-heading": "Error interno del servidor",
"500-page-text": "Se produjo un error interno del servidor. Lo sentimos :/",
"fulltext-search-unavailable": "Búsqueda de texto completo no disponible",
"no-search-results": "El motor de búsqueda de texto completo no está disponible para este contenido.",
"search-results-page-title": "Buscar: {{SEARCH_PATTERN}}",
@@ -53,9 +37,9 @@
"search-result-book-info": "a partir de {{BOOK_TITLE}}",
"word-count": "{{COUNT}} palabras",
"library-button-text": "Ir a la página de bienvenida",
"home-button-text": "Ir a la página principal de '{{{BOOK_TITLE}}}'",
"home-button-text": "Ir a la página principal de '{{BOOK_TITLE}}'",
"random-page-button-text": "Ir a una página seleccionada al azar",
"searchbox-tooltip": "Buscar '{{{BOOK_TITLE}}}'",
"searchbox-tooltip": "Buscar '{{BOOK_TITLE}}'",
"confusion-of-tongues": "Dos o más libros en diferentes idiomas participarían en la búsqueda, lo que puede llevar a resultados confusos.",
"welcome-page-overzealous-filter": "Sin resultados. ¿Quieres <a href=\"{{URL}}\">restablecer el filtro</a> ?",
"powered-by-kiwix-html": "Desarrollado por&nbsp;<a href=\"https://kiwix.org\">Kiwix</a>",
@@ -81,15 +65,7 @@
"download-links-title": "Descargar libro",
"preview-book": "Previsualizar",
"unknown-error": "Error desconocido",
"book-category.gutenberg": "Gutenberg",
"book-category.iFixit": "iFixit",
"book-category.mooc": "MOOC",
"book-category.phet": "Phet",
"book-category.stack_exchange": "Stack Exchange",
"book-category.ted": "Ted",
"book-category.vikidia": "Vikidia",
"book-category.wikibooks": "Wikilibros",
"book-category.wikihow": "wikiHow",
"book-category.wikinews": "Wikinoticias",
"book-category.wikipedia": "Wikipedia",
"book-category.wikiquote": "Wikiquote",
@@ -98,6 +74,5 @@
"book-category.wikiversity": "Wikiversidad",
"book-category.wikivoyage": "Wikiviajes",
"book-category.wiktionary": "Wikcionario",
"book-category.other": "Otros",
"text-loading-content": "Cargando contenido"
"book-category.other": "Otros"
}

View File

@@ -3,10 +3,10 @@
"authors": [
"MITO",
"Nike",
"Pyscowicz",
"Samoasambia"
"Pyscowicz"
]
},
"name": "suomi",
"suggest-full-text-search": "sisältää '{{{SEARCH_TERMS}}}'...",
"no-such-book": "Kirjaa {{BOOK_NAME}} ei ole olemassa",
"url-not-found": "Pyydettyä URL-osoitetta \"{{url}}\" ei löytynyt tältä palvelimelta.",
@@ -14,22 +14,13 @@
"400-page-heading": "Virheellinen pyyntö",
"404-page-title": "Sisältöä ei löytynyt",
"404-page-heading": "Ei löytynyt",
"new-404-page-title": "Sivua ei löytynyt",
"new-404-page-heading": "Hupsista. Artikkelia ei löytynyt.",
"404-img-text": "Ei löytynyt!",
"path-was-not-found": "Pyydettyä polkua ei löytynyt:",
"500-page-title": "Sisäinen palvelinvirhe",
"500-page-heading": "Hupsista. Sivu ei toimi.",
"500-img-text": "Sivu ei toimi",
"external-link-detected": "Ulkoinen linkki havaittu",
"caution-warning": "Huomio!",
"external-link-intro": "Olet poistumassa Kiwixin ZIM-lukijasta siirtyäksesi verkkoon",
"search-results-page-title": "Haku: {{SEARCH_PATTERN}}",
"500-page-heading": "Sisäinen palvelinvirhe",
"word-count": "{{COUNT}} sanaa",
"library-button-text": "Siirry tervetulosivulle",
"home-button-text": "Siirry kirjan '{{{BOOK_TITLE}}}' etusivulle",
"home-button-text": "Siirry kirjan '{{BOOK_TITLE}}' etusivulle",
"random-page-button-text": "Siirry satunnaiselle sivulle",
"searchbox-tooltip": "Hae kirjasta '{{{BOOK_TITLE}}}'",
"searchbox-tooltip": "Hae '{{BOOK_TITLE}}'",
"search": "Hae",
"book-filtering-all-categories": "Kaikki luokat",
"book-filtering-all-languages": "Kaikki kielet",
@@ -37,21 +28,10 @@
"download": "Lataa",
"magnet-link-text": "Magnet-linkki",
"magnet-alt-text": "lataa magnet",
"torrent-download-link-text": "BitTorrent",
"torrent-download-link-text": "Torrent-tiedosto",
"torrent-download-alt-text": "lataa torrent-tiedosto",
"filter-by-tag": "Suodata tunnisteen ”{{TAG}}” mukaan",
"download-links-title": "Lataa kirja",
"preview-book": "Esikatsele",
"unknown-error": "Tuntematon virhe",
"book-category.wikibooks": "Wikikirjasto",
"book-category.wikinews": "Wikiuutiset",
"book-category.wikipedia": "Wikipedia",
"book-category.wikiquote": "Wikisitaatit",
"book-category.wikisource": "Wikiaineisto",
"book-category.wikispecies": "Wikispecies",
"book-category.wikiversity": "Wikiopisto",
"book-category.wikivoyage": "Wikimatkat",
"book-category.wiktionary": "Wikisanakirja",
"book-category.other": "Muu",
"text-loading-content": "Ladataan sisältöä"
"unknown-error": "Tuntematon virhe"
}

View File

@@ -3,7 +3,6 @@
"authors": [
"Adriendelucca",
"Benoit74",
"Derugon",
"Gomoko",
"Goombiis",
"Melimeli",
@@ -15,6 +14,7 @@
"Wladek92"
]
},
"name": "Français",
"suggest-full-text-search": "contenant « {{{SEARCH_TERMS}}} »...",
"no-such-book": "Aucun livre avec ce nom: {{BOOK_NAME}}",
"too-many-books": "Trop de livres demandés ({{NB_BOOKS}}) alors que la limite est de {{LIMIT}}",
@@ -31,25 +31,9 @@
"400-page-heading": "Requête non valide",
"404-page-title": "Contenu non trouvé",
"404-page-heading": "Non trouvé",
"new-404-page-title": "Page non trouvée",
"new-404-page-heading": "Oups. Page non trouvée.",
"404-img-text": "Non trouvée !",
"path-was-not-found": "Le chemin demandé na pas été trouvé :",
"404-advice.p1": "Le contenu que vous recherchez peut toujours être disponible, mais il peut être positionné à un endroit différent dans le fichier ZIM.",
"404-advice.p2": "Veuillez :",
"404-advice.p3": "Essayer dutiliser la fonction de recherche pour trouver le contenu que vous souhaitez",
"404-advice.p4": "Rechercher des mots-clés ou des titres liés aux informations que vous recherchez",
"404-advice.p5": "Cette approche devrait vous aider à localiser le contenu souhaité, même si le lien dorigine ne fonctionne pas correctement.",
"500-page-title": "Erreur interne du serveur",
"500-page-heading": "Oups, la page ne fonctionne pas.",
"500-page-text": "Le chemin demandé ne peut pas être fourni correctement :",
"500-img-text": "La page ne fonctionne pas",
"external-link-detected": "Lien externe détecté",
"caution-warning": "Attention!",
"external-link-intro": "Vous êtes sur le point de quitter le lecteur ZIM de Kiwix pour vous connecter à",
"external-link-advice.p1": "Le lien que vous essayez de suivre ne fait pas partie de votre forfait hors ligne et nécessite une connexion Internet.",
"external-link-advice.p2": "Si vous pouvez vous connecter en ligne, vous pouvez essayer douvrir le lien.",
"external-link-advice.p3": "Vous pouvez également revenir au contenu hors ligne de votre ZIM en utilisant le bouton Retour de votre navigateur.",
"500-page-heading": "Erreur interne du serveur",
"500-page-text": "Une erreur de serveur interne s'est produite. Nous en sommes désolés :/",
"fulltext-search-unavailable": "Recherche en texte intégral non disponible",
"no-search-results": "Le moteur de recherche en texte intégral nest pas disponible pour ce contenu.",
"search-results-page-title": "Rechercher : {{SEARCH_PATTERN}}",
@@ -58,9 +42,9 @@
"search-result-book-info": "à partir de {{BOOK_TITLE}}",
"word-count": "{{COUNT}} mots",
"library-button-text": "Aller à la page de bienvenue",
"home-button-text": "Aller à la page principale de « {{{BOOK_TITLE}}} »",
"home-button-text": "Aller à la page principale de « {{BOOK_TITLE}} »",
"random-page-button-text": "Aller à une page sélectionnée aléatoirement",
"searchbox-tooltip": "Rechercher « {{{BOOK_TITLE}}} »",
"searchbox-tooltip": "Rechercher « {{BOOK_TITLE}} »",
"confusion-of-tongues": "Deux livres ou plus dans des langues différentes participeraient à la recherche, ce qui pourrait conduire à des résultats confus.",
"welcome-page-overzealous-filter": "Aucun résultat. Souhaitez-vous <a href=\"{{URL}}\">réinitialiser le filtre</a>?",
"powered-by-kiwix-html": "Propulsé par <a href=\"https://kiwix.org/\">Kiwix</a>",

View File

@@ -8,6 +8,7 @@
"Yusuf Sa'adu"
]
},
"name": "Turanci",
"suggest-full-text-search": "dauke da ''{{{SEARCH_TERMS}}}''...",
"no-such-book": "Babu irin wannan littafin: {{BOOK_NAME}}",
"too-many-books": "An nemi littattafai da yawa ({{NB_BOOKS}}) inda iyaka shine {{LIMIT}}",

View File

@@ -5,6 +5,7 @@
"YaronSh"
]
},
"name": "עברית",
"suggest-full-text-search": "מכיל '{{{SEARCH_TERMS}}}'...",
"no-such-book": "אין ספר כזה: {{BOOK_NAME}}",
"too-many-books": "נתבקשו יותר ספרים ({{NB_BOOKS}}) והמגבלה היא {{LIMIT}}",
@@ -21,19 +22,9 @@
"400-page-heading": "בקשה בלתי־תקינה",
"404-page-title": "התוכן לא נמצא",
"404-page-heading": "לא נמצא",
"new-404-page-title": "הדף לא נמצא",
"new-404-page-heading": "אופס. הדף לא נמצא.",
"404-img-text": "לא נמצא!",
"path-was-not-found": "הנתיב המבוקש לא נמצא:",
"404-advice.p2": "בבקשה:",
"404-advice.p3": "לנסות להשתמש ביכולת החיפוש כדי למצוא את התוכן המבוקש",
"500-page-title": "שגיאת שרת פנימית",
"500-page-heading": "אופס. הדף לא עובד.",
"500-page-text": "אי אפשר למסור את הנתיב המבוקש כראוי:",
"500-img-text": "הדף לא עובד",
"external-link-detected": "התגלה קישור חיצוני",
"caution-warning": "זהירות!",
"external-link-advice.p2": "אם יש לך איך להתחבר לאינטרנט, אפשר לנסות לפתוח את הקישור.",
"500-page-heading": "שגיאת שרת פנימית",
"500-page-text": "אירעה שגיאת שרת פנימית. אנחנו מצטערים על זה :/",
"fulltext-search-unavailable": "חיפוש בטקסט מלא אינו זמין",
"no-search-results": "מנוע החיפוש בטקסט מלא אינו זמין עבור התוכן הזה.",
"search-results-page-title": "חיפוש: {{SEARCH_PATTERN}}",
@@ -42,9 +33,9 @@
"search-result-book-info": "מתוך {{BOOK_TITLE}}",
"word-count": "{{COUNT}} מילים",
"library-button-text": "מעבר לדף הבית \"ברוך בואך\"",
"home-button-text": "מעבר לדף הראשי של {{{BOOK_TITLE}}}",
"home-button-text": "מעבר לדף הראשי של \"{{BOOK_TITLE}}\"",
"random-page-button-text": "מעבר לדף שנבחר אקראית",
"searchbox-tooltip": "חיפוש אחר {{{BOOK_TITLE}}}",
"searchbox-tooltip": "חיפוש \"{{BOOK_TITLE}}\"",
"confusion-of-tongues": "שני ספרים או יותר בשפות שונות ישתתפו בחיפוש, מה שעלול להוביל לתוצאות מבלבלות.",
"welcome-page-overzealous-filter": "אין תוצאות. האם <a href=\"{{URL}}\">לאפס את המסנן</a>?",
"powered-by-kiwix-html": "מופעל על־ידי&nbsp;<a href=\"https://kiwix.org\">Kiwix</a>",
@@ -81,6 +72,5 @@
"book-category.wikiversity": "ויקיברסיטה",
"book-category.wikivoyage": "ויקימסע",
"book-category.wiktionary": "ויקימילון",
"book-category.other": "אחר",
"text-loading-content": "התוכן נטען"
"book-category.other": "אחר"
}

View File

@@ -5,6 +5,7 @@
"Juuz0"
]
},
"name": "हिन्दी",
"suggest-full-text-search": "जिसमें '{{{SEARCH_TERMS}}}' शामिल है...",
"no-such-book": "ऐसी कोई किताब नहीं: {{BOOK_NAME}}",
"too-many-books": "बहुत सारी पुस्तकों का अनुरोध किया गया है ({{NB_BOOKS}}) जहां सीमा {{LIMIT}} है",

View File

@@ -1,38 +0,0 @@
{
"@metadata": {
"authors": [
"Eukarióta",
"Urbalazs"
]
},
"400-page-title": "Érvénytelen kérés",
"400-page-heading": "Érvénytelen kérés",
"404-page-title": "A tartalom nem található",
"404-page-heading": "Nem található",
"new-404-page-title": "Az oldal nem található",
"new-404-page-heading": "Hoppá! Az oldal nem található.",
"404-img-text": "Nem található!",
"path-was-not-found": "A kért útvonal nem található:",
"404-advice.p1": "A keresett tartalom továbbra is elérhető lehet, de előfordulhat, hogy más helyen található a ZIM-fájlon belül.",
"search": "Keresés",
"download": "Letöltés",
"hash-download-link-text": "SHA-256 ellenőrzőösszeg",
"hash-download-alt-text": "SHA-256 fájl-ellenőrzőösszeg megjelenítése",
"torrent-download-link-text": "BitTorrent",
"welcome-to-kiwix-server": "Üdvözli a Kiwix-kiszolgáló!",
"download-links-heading": "Letöltési hivatkozások ehhez: <b><i>{{BOOK_TITLE}}</i></b>",
"download-links-title": "Könyv letöltése",
"preview-book": "Előnézet",
"unknown-error": "Ismeretlen hiba",
"book-category.wikibooks": "Wikikönyvek",
"book-category.wikinews": "Wikihírek",
"book-category.wikipedia": "Wikipédia",
"book-category.wikiquote": "Wikidézet",
"book-category.wikisource": "Wikiforrás",
"book-category.wikispecies": "Wikifajok",
"book-category.wikiversity": "Wikiegyetem",
"book-category.wikivoyage": "Wikivoyage",
"book-category.wiktionary": "Wikiszótár",
"book-category.other": "Egyéb",
"text-loading-content": "Tartalom betöltése"
}

View File

@@ -4,6 +4,7 @@
"Kareyac"
]
},
"name": "Հայերեն",
"suggest-full-text-search": "որոնել '{{{SEARCH_TERMS}}}'...",
"no-such-book": "Գիրքը բացակայում է՝ {{BOOK_NAME}}",
"url-not-found": "Սխալ հասցե՝ {{url}}",
@@ -12,7 +13,6 @@
"400-page-heading": "Անվավեր հարցում",
"404-page-title": "Սխալ հասցե",
"404-page-heading": "Սխալ հասցե",
"500-img-text": "Էջը չի աշխատում",
"library-button-text": "Գրադարանի էջ",
"home-button-text": "Դեպի '{{BOOK_TITLE}}'֊ի գլխավոր էջը",
"random-page-button-text": "Բացել պատահական էջ",

View File

@@ -5,6 +5,7 @@
"McDutchie"
]
},
"name": "interlingua",
"suggest-full-text-search": "continente '{{{SEARCH_TERMS}}}'...",
"no-such-book": "Necun tal libro: {{BOOK_NAME}}",
"too-many-books": "Troppo de libros demandate ({{NB_BOOKS}}); le limite es {{LIMIT}}",
@@ -21,25 +22,9 @@
"400-page-heading": "Requesta invalide",
"404-page-title": "Contento non trovate",
"404-page-heading": "Non trovate",
"new-404-page-title": "Pagina non trovate",
"new-404-page-heading": "Pagina non trovate.",
"404-img-text": "Non trovate!",
"path-was-not-found": "Le percurso requestate non ha essite trovate:",
"404-advice.p1": "Le contento que tu cerca pote esser ancora disponibile, ma illo pote esser situate alterubi in le file ZIM.",
"404-advice.p2": "Per favor:",
"404-advice.p3": "Tenta usar le function de recerca pro trovar le contento desirate",
"404-advice.p4": "Cerca parolas-clave o titulos associate al information que tu cerca",
"404-advice.p5": "Iste approche deberea adjutar te a localisar le contento desirate, mesmo si le ligamine original non functiona correctemente.",
"500-page-title": "Error interne del servitor",
"500-page-heading": "Le pagina non functiona.",
"500-page-text": "Le percurso requestate non pote esser livrate correctemente:",
"500-img-text": "Le pagina non functiona",
"external-link-detected": "Ligamine externe detegite",
"caution-warning": "Attention!",
"external-link-intro": "Tu es sur le puncto de quitar le lector ZIM de Kiwix pro connecter te al rete e visitar",
"external-link-advice.p1": "Le ligamine al qual tu tenta acceder non face parte de nostre pacchetto de uso foras de linea e require un connexion a internet.",
"external-link-advice.p2": "Si tu pote connecter te al rete, tu pote tentar aperir le ligamine.",
"external-link-advice.p3": "Si non, tu pote retornar al contento de uso foras de linea de tu ZIM usante le button Retro del navigator.",
"500-page-heading": "Error interne del servitor",
"500-page-text": "Un error interne del servitor ha occurrite. Nos lo regretta :/",
"fulltext-search-unavailable": "Le recerca in texto complete es indisponibile",
"no-search-results": "Le motor de recerca in texto complete non es disponibile pro iste contento.",
"search-results-page-title": "Cercar: {{SEARCH_PATTERN}}",
@@ -48,9 +33,9 @@
"search-result-book-info": "de {{BOOK_TITLE}}",
"word-count": "{{COUNT}} parolas",
"library-button-text": "Ir al pagina de benvenita",
"home-button-text": "Ir al pagina principal de {{{BOOK_TITLE}}}",
"home-button-text": "Ir al pagina principal de ''{{BOOK_TITLE}}",
"random-page-button-text": "Ir a un pagina seligite aleatorimente",
"searchbox-tooltip": "Cercar in {{{BOOK_TITLE}}}",
"searchbox-tooltip": "Cercar '{{BOOK_TITLE}}'",
"confusion-of-tongues": "Duo o plus libros in differente linguas participarea in le recerca, lo que pote menar a resultatos confuse.",
"welcome-page-overzealous-filter": "Nulle resultato. Vole tu <a href=\"{{URL}}\">reinitialisar le filtro</a>?",
"powered-by-kiwix-html": "Actionate per&nbsp;<a href=\"https://kiwix.org\">Kiwix</a>",
@@ -85,6 +70,5 @@
"book-category.wikiversity": "Wikiversitate",
"book-category.wikivoyage": "Wikiviage",
"book-category.wiktionary": "Wiktionario",
"book-category.other": "Altere",
"text-loading-content": "Carga contento"
"book-category.other": "Altere"
}

View File

@@ -4,6 +4,7 @@
"Akmaie Ajam"
]
},
"name": "Bahasa Inggris",
"suggest-full-text-search": "mengandung '{{{SEARCH_TERMS}}}'...",
"no-such-book": "Tidak ada buku seperti ini: {{BOOK_NAME}}",
"too-many-books": "Terlalu banyak buku yang diminta ({{NB_BOOKS}}) dimana batasnya adalah {{LIMIT}}",

View File

@@ -6,6 +6,7 @@
"Oby Ezeilo"
]
},
"name": "Bekee",
"suggest-full-text-search": "nwere {{{SEARCH_TERMS}}}'",
"no-such-book": "Enweghị akwụkwọ dị otú a: {{BOOK_NAME}}",
"too-many-books": "Arịrịọ ọtụtụ akwụkwọ ({{NB_BOOKS}}) ebe oke bụ {{LIMIT}}",

View File

@@ -5,10 +5,10 @@
"Beta16",
"Clorofolle",
"Luca.favorido",
"McDutchie",
"Wheelygay"
"McDutchie"
]
},
"name": "italiano",
"suggest-full-text-search": "contenente '{{{SEARCH_TERMS}}}'...",
"no-such-book": "Nessun libro con questo nome: {{BOOK_NAME}}",
"too-many-books": "Troppi libri richiesti ({{NB_BOOKS}}) dove il limite è {{LIMIT}}",
@@ -26,18 +26,17 @@
"new-404-page-heading": "Oops. Pagina non trovata.",
"404-img-text": "Non trovato!",
"500-page-title": "Errore interno del server",
"500-page-heading": "Oops. La pagina non funziona.",
"500-page-text": "Il percorso richiesto non può essere fornito correttamente:",
"caution-warning": "Attenzione!",
"500-page-heading": "Errore interno del server",
"500-page-text": "Si è verificato un errore interno del server. Ci dispiace :/",
"search-results-page-title": "Cerca: {{SEARCH_PATTERN}}",
"search-results-page-header": "Risultati <b>{{START}}-{{END}}</b> di <b>{{COUNT}}</b> per <b>\"{{{SEARCH_PATTERN}}}\"</b>",
"empty-search-results-page-header": "Non è stato trovato alcun risultato per <b>\"{{{SEARCH_PATTERN}}}\"</b>",
"search-result-book-info": "da {{BOOK_TITLE}}",
"word-count": "{{COUNT}} parole",
"library-button-text": "Vai alla pagina di benvenuto",
"home-button-text": "Vai alla pagina principale di '{{{BOOK_TITLE}}}'",
"home-button-text": "Vai alla pagina principale di '{{BOOK_TITLE}}'",
"random-page-button-text": "Vai a una pagina selezionata casualmente",
"searchbox-tooltip": "Cerca '{{{BOOK_TITLE}}}'",
"searchbox-tooltip": "Cerca '{{BOOK_TITLE}}'",
"welcome-page-overzealous-filter": "Nessun risultato. Vuoi <a href=\"{{URL}}\">reimpostare il filtro</a>?",
"search": "Cerca",
"book-filtering-all-categories": "Tutte le categorie",

View File

@@ -5,6 +5,7 @@
"もなー(偽物)"
]
},
"name": "日本語",
"no-query": "クエリを指定していません。",
"400-page-title": "無効なリクエストです",
"400-page-heading": "無効なリクエストです",

View File

@@ -1,10 +1,10 @@
{
"@metadata": {
"authors": [
"YeBoy371",
"Ykhwong"
]
},
"name": "한국어",
"suggest-full-text-search": "'{{{SEARCH_TERMS}}}' 포함...",
"no-such-book": "해당 책이 없습니다: {{BOOK_NAME}}",
"too-many-books": "요청된 책이 너무 많습니다. ({{NB_BOOKS}}) 한도는 {{LIMIT}}입니다.",
@@ -21,17 +21,9 @@
"400-page-heading": "잘못된 요청",
"404-page-title": "내용이 없습니다",
"404-page-heading": "찾을 수 없음",
"new-404-page-title": "페이지를 찾을 수 없습니다",
"new-404-page-heading": "이런. 페이지를 찾을 수 없습니다.",
"404-img-text": "찾을 수 없습니다!",
"path-was-not-found": "요청한 경로를 찾을 수 없습니다:",
"404-advice.p3": "원하는 콘텐츠를 찾으려면 검색 기능을 사용해 보세요",
"500-page-title": "내부 서버 오류",
"500-page-heading": "죄송합니다. 문서가 동작하지 않습니다.",
"500-page-text": "요청된 경로를 제대로 전달할 수 없습니다:",
"500-img-text": "문서가 동작하지 않습니다",
"external-link-detected": "외부 링크가 발견되었습니다",
"caution-warning": "경고!",
"500-page-heading": "내부 서버 오류",
"500-page-text": "내부 서버 오류가 발생했습니다. 죄송합니다 :/",
"fulltext-search-unavailable": "전문 검색을 사용할 수 없습니다",
"no-search-results": "이 콘텐츠에는 전문 검색 엔진을 사용할 수 없습니다.",
"search-results-page-title": "검색: {{SEARCH_PATTERN}}",
@@ -39,9 +31,8 @@
"empty-search-results-page-header": "<b>\"{{{SEARCH_PATTERN}}}\"</b>의 결과가 없습니다",
"search-result-book-info": "{{BOOK_TITLE}}에서",
"word-count": "단어 {{COUNT}}개",
"home-button-text": "'{{{BOOK_TITLE}}}'의 메인 페이지로 이동",
"random-page-button-text": "무작위로 선택된 문서로 이동",
"searchbox-tooltip": "'{{{BOOK_TITLE}}}' 검색",
"searchbox-tooltip": "'{{BOOK_TITLE}}' 검색",
"welcome-page-overzealous-filter": "결과가 없습니다. <a href=\"{{URL}}\">필터를 재설정</a>하시겠습니까?",
"powered-by-kiwix-html": "<a href=\"https://kiwix.org\">Kiwix</a>에서 제공",
"search": "검색",

View File

@@ -4,6 +4,7 @@
"Bikarhêner"
]
},
"name": "kurdî",
"suggest-full-text-search": "'{{{SEARCH_TERMS}}}' dihewîne...",
"no-such-book": "Kitêbeke wisa nîne: {{BOOK_NAME}}",
"too-many-books": "Pir zêde kitêb hatiye xwestin ({{NB_BOOKS}}) ku sînor {{LIMIT}} ye",

View File

@@ -6,6 +6,7 @@
"Volvox"
]
},
"name": "Lëtzebuergesch",
"suggest-full-text-search": "enthält '{{{SEARCH_TERMS}}}'...",
"no-such-book": "Buch net fonnt: {{BOOK_NAME}}",
"too-many-books": "Ze vill Bicher ugefrot ({{NB_BOOKS}}), d'Limitt läit bei {{LIMIT}}",
@@ -18,10 +19,8 @@
"new-404-page-heading": "Ups. Säit net fonnt.",
"404-img-text": "Net fonnt!",
"500-page-title": "Interne Feeler um Server",
"500-page-heading": "Ups. D'Säit funktionéiert net.",
"500-page-heading": "Interne Feeler um Server",
"500-page-text": "Et ass en interne Serverfeeler opgetrueden. Mir entschëllegen eis dofir :/",
"500-img-text": "Säit funktionéiert net",
"external-link-detected": "Externe Link entdeckt",
"caution-warning": "Opgepasst!",
"fulltext-search-unavailable": "Volltext-Sich net verfügbar",
"search-results-page-title": "Sichen: {{SEARCH_PATTERN}}",
@@ -29,9 +28,9 @@
"empty-search-results-page-header": "Keng Resultater fonnt fir <b>„{{{SEARCH_PATTERN}}}“</b>",
"search-result-book-info": "aus {{BOOK_TITLE}}",
"word-count": "{{COUNT}} Wierder",
"home-button-text": "Op d'Haaptsäit vun '{{{BOOK_TITLE}}}' goen",
"home-button-text": "Gitt op d'Haaptsäit vun '{{BOOK_TITLE}}'",
"random-page-button-text": "Gitt op eng zoufälleg gewielte Säit",
"searchbox-tooltip": "No '{{{BOOK_TITLE}}}' sichen",
"searchbox-tooltip": "No '{{BOOK_TITLE}}' sichen",
"welcome-page-overzealous-filter": "Kee Resultat. Wëllt Dir <a href=\"{{URL}}\">de Filter zrécksetzen</a>?",
"search": "Sichen",
"book-filtering-all-categories": "All Kategorien",

View File

@@ -5,6 +5,7 @@
"Kelson"
]
},
"name": "македонски",
"suggest-full-text-search": "содржи „{{{SEARCH_TERMS}}}“...",
"no-such-book": "Нема книга нарчена {{BOOK_NAME}}",
"too-many-books": "Побаравте премногу книги ({{NB_BOOKS}}). Ограничени сте на {{LIMIT}}",
@@ -21,14 +22,9 @@
"400-page-heading": "Неважечко барање",
"404-page-title": "Содржината не е најдена",
"404-page-heading": "Не е најдено",
"new-404-page-title": "Страницата не е пронајдена",
"new-404-page-heading": "Ах! Страницата не е пронајдена.",
"404-img-text": "Не е најдено!",
"path-was-not-found": "Не ја најдов побараната патека:",
"500-page-title": "Внатрешна грешка во опслужувачот",
"500-page-heading": "Внатрешна грешка во опслужувачот",
"500-page-text": "Настана внатрешна грешка во опслужувачот. Жал ни е :/",
"caution-warning": "Внимание!",
"fulltext-search-unavailable": "Целотекстното пребарување е недостапно",
"no-search-results": "Погонот за целотекстно пребарување не е достапен за оваа содржина.",
"search-results-page-title": "Пребарување: {{SEARCH_PATTERN}}",

View File

@@ -4,6 +4,7 @@
"Tofeiku"
]
},
"name": "Bahasa Melayu",
"404-page-heading": "Tidak Dijumpai",
"500-page-title": "Ralat Pelayan Dalaman",
"500-page-heading": "Ralat Pelayan Dalaman",

View File

@@ -4,6 +4,7 @@
"TorgeirS"
]
},
"name": "Engelsk",
"suggest-full-text-search": "inneholder '{{{SEARCH_TERMS}}}'...",
"no-such-book": "Finner ingen slik bok: {{BOOK_NAME}}",
"too-many-books": "Det er forespurt for mange bøker {{NB_BOOKS}} der grensen er {{LIMIT}}",

View File

@@ -8,6 +8,7 @@
"Vistaus"
]
},
"name": "Nederlands",
"suggest-full-text-search": "bevat {{{SEARCH_TERMS}}}’…",
"no-such-book": "Boek bestaat niet: {{BOOK_NAME}}",
"too-many-books": "Er zijn teveel boeken opgevraagd ({{NB_BOOKS}}). De limiet is {{LIMIT}}.",
@@ -24,25 +25,9 @@
"400-page-heading": "Ongeldig verzoek",
"404-page-title": "Inhoud niet gevonden",
"404-page-heading": "Niet gevonden",
"new-404-page-title": "Pagina niet gevonden",
"new-404-page-heading": "Oeps. Pagina niet gevonden.",
"404-img-text": "Niet gevonden!",
"path-was-not-found": "Het gevraagde pad is niet gevonden:",
"404-advice.p1": "De inhoud die u zoekt is mogelijk toch beschikbaar, maar kan zich ergens anders in het ZIM-bestand bevinden.",
"404-advice.p2": "U kunt:",
"404-advice.p3": "De zoekfunctie proberen om de gewenste inhoud te vinden",
"404-advice.p4": "Trefwoorden of titels opzoeken die verband houden met de informatie die u zoekt",
"404-advice.p5": "Met deze aanpak kunt u hopelijk de gewenste inhoud vinden, ook als de oorspronkelijke koppeling niet goed werkt.",
"500-page-title": "Interne serverfout",
"500-page-heading": "Oeps. De pagina werkt niet.",
"500-page-text": "Het aangevraagde pad kan niet goed beschikbaar worden gesteld:",
"500-img-text": "Pagina werkt niet",
"external-link-detected": "Externe koppeling gedetecteerd",
"caution-warning": "Voorzichtig!",
"external-link-intro": "U staat op het punt de ZIM-lezer van Kiwix te verlaten om online te gaan naar",
"external-link-advice.p1": "De koppeling die u probeert te openen, maakt geen deel uit van uw offline-pakket en vereist een internetverbinding.",
"external-link-advice.p2": "Als u online kunt gaan, kunt u proberen de koppeling te openen.",
"external-link-advice.p3": "Anders kunt u met de terugknop van uw browser terugkeren naar de offline-inhoud van uw ZIM.",
"500-page-heading": "Interne serverfout",
"500-page-text": "Er is een interne serverfout opgetreden. Dat spijt ons",
"fulltext-search-unavailable": "Zoeken in volledige tekst is niet beschikbaar",
"no-search-results": "De zoekmachine voor volledige tekst is niet beschikbaar voor deze inhoud.",
"search-results-page-title": "Zoeken: {{SEARCH_PATTERN}}",
@@ -51,9 +36,9 @@
"search-result-book-info": "uit {{BOOK_TITLE}}",
"word-count": "{{COUNT}} woorden",
"library-button-text": "Naar de welkomstpagina",
"home-button-text": "Naar de hoofdpagina van {{{BOOK_TITLE}}}",
"home-button-text": "Naar de hoofdpagina van {{BOOK_TITLE}}",
"random-page-button-text": "Naar een willekeurig geselecteerde pagina gaan",
"searchbox-tooltip": "{{{BOOK_TITLE}}} doorzoeken",
"searchbox-tooltip": "Naar {{BOOK_TITLE}} zoeken",
"confusion-of-tongues": "Er zouden twee of meer boeken in verschillende talen deelnemen aan de zoekopdracht, wat tot verwarrende resultaten kan leiden.",
"welcome-page-overzealous-filter": "Geen resultaat. Wilt u <a href=\"{{URL}}\">het filter resetten</a>?",
"powered-by-kiwix-html": "Mogelijk gemaakt door <a href=\"https://kiwix.org\">Kiwix</a>",

View File

@@ -4,6 +4,7 @@
"Lancine.kounfantoh.fofana"
]
},
"name": "ߒߞߏ",
"suggest-full-text-search": "ߞߣߐߞߍߣߍ߲߫ ߦߋ߫ '{{{SEARCH_TERMS}}}'...",
"no-such-book": "ߞߊ߬ߝߊ߫ ߛߎ߮ ߏ߬ ߕߴߦߋ߲߬: {{BOOK_NAME}}",
"too-many-books": "ߞߝߊ߬ ߛߌߦߊߡߊ߲߫ ߡߊߢߌ߬ߣߌ߲߬ߞߊ߬ߣߍ߲߫ ߞߏߖߎ߰ {{NB_BOOKS}} ߡߍ߲ ߞߐߘߊ߲ ߦߋ߫ {{LIMIT}} ߘߌ߫",

View File

@@ -5,6 +5,7 @@
"Psubhashish"
]
},
"name": "ଓଡ଼ିଆ",
"suggest-full-text-search": "${{{SEARCH_TERMS}}} ଧାରଣ କରିଛି ...",
"no-such-book": "ଏପରି କୌଣସି ପୁସ୍ତକ ନାହିଁଃ ${{BOOK_NAME}}",
"too-many-books": "ଅତ୍ୟଧିକ ବହି ଅନୁରୋଧ (${{NB_BOOKS}}) ଯେଉଁଠାରେ ସୀମା ${{LIMIT}} |",

View File

@@ -5,6 +5,7 @@
"WaldiSt"
]
},
"name": "Polski",
"suggest-full-text-search": "zawierający '{{{SEARCH_TERMS}}}'...",
"no-such-book": "Brak takiej książki: {{BOOK_NAME}}",
"url-not-found": "Żądany adres URL „{{url}}” nie został znaleziony na tym serwerze.",

View File

@@ -8,6 +8,7 @@
"YoReaper"
]
},
"name": "Português",
"suggest-full-text-search": "Contendo '{{{SEARCH_TERMS}}}'...",
"no-such-book": "Não existe o livro: {{BOOK_NAME}}",
"too-many-books": "Muitos livros solicitados {{NB_BOOKS}} mas o limite é {{LIMIT}}",
@@ -26,7 +27,6 @@
"500-page-title": "Erro interno do servidor",
"500-page-heading": "Erro interno do servidor",
"500-page-text": "Aconteceu um erro interno do servidor. Nós pedimos desculpas sobre isso :/",
"caution-warning": "Cuidado!",
"fulltext-search-unavailable": "Busca por texto completo está indisponível",
"no-search-results": "O motor de busca de texto completo não está disponível para este conteúdo.",
"search-results-page-title": "Buscar: {{SEARCH_PATTERN}}",

View File

@@ -4,6 +4,7 @@
"B3rnas"
]
},
"name": "português",
"suggest-full-text-search": "contendo '{{{SEARCH_TERMS}}}'...",
"no-such-book": "Não existe o livro: {{BOOK_NAME}}",
"too-many-books": "Demasiadas solicitações de livros ({{NB_BOOKS}}) onde o limite é {{LIMIT}}",

View File

@@ -7,6 +7,7 @@
"Verdy p"
]
},
"name": "{{Doc-important|Don't write \"English\" in your language!}}\n\n'''Write the name of ''your'' language in its native script.'''\n\nCurrent language to which the string is being translated to.\n\nFor example, write \"français\" when translating to French, or \"Deutsch\" when translating to German.\n\n'''Important:''' Do not use your languages word for “English”. Use the word that your language uses to refer to itself. If you translate this message to mean “English” in your language, your change will be reverted.",
"suggest-full-text-search": "Text appearing in the suggestion list that, when selected, runs a full text search instead of the title search",
"no-such-book": "Error text when the requested book is not found in the library",
"too-many-books": "Error text when user request more books than the limit set by the administrator",
@@ -35,7 +36,6 @@
"500-page-title": "Title of the 500 error page",
"500-page-heading": "Heading of the 500 error page",
"500-page-text": "Text of the 500 error page",
"500-img-text": "Fallback text for the image on the 500 error page",
"external-link-detected": "Title & heading of the external link blocker page",
"caution-warning": "Warning of action that shouldn't be carried out carelessly",
"external-link-intro": "Message introducing the external link (to be followed by the actual link)",

View File

@@ -5,6 +5,7 @@
"Trotinel Iftode"
]
},
"name": "Engleză",
"suggest-full-text-search": "care conține '{{{SEARCH_TERMS}}}'...",
"no-such-book": "Nu există o astfel de carte: {{BOOK_NAME}}",
"too-many-books": "Prea multe cărți solicitate ({{NB_BOOKS}}) unde limita este {{LIMIT}}",

View File

@@ -11,6 +11,7 @@
"Smavrina"
]
},
"name": "русский",
"suggest-full-text-search": "содержащее '{{{SEARCH_TERMS}}}'...",
"no-such-book": "Такой книги нет: {{BOOK_NAME}}",
"too-many-books": "Запрошено слишком много книг ({{NB_BOOKS}}), максимальное количество — {{LIMIT}}.",

View File

@@ -5,6 +5,7 @@
"L2212"
]
},
"name": "Sardu",
"suggest-full-text-search": "chi cuntenet '{{{SEARCH_TERMS}}}'...",
"no-such-book": "Perunu libru cun custu nùmene: {{BOOK_NAME}}",
"too-many-books": "Tropu libros pedidos, {{NB_BOOKS}} cando su lìmite est de {{LIMIT}}",

View File

@@ -4,6 +4,7 @@
"Yardom78"
]
},
"name": "slovenčina",
"suggest-full-text-search": "obsahuje '{{{SEARCH_TERMS}}}'...",
"no-such-book": "Žiadna kniha ako: {{BOOK_NAME}}",
"too-many-books": "Príliš veľa požadovaných kníh ({{NB_BOOKS}}), limit je {{LIMIT}}",

View File

@@ -4,6 +4,7 @@
"Saraiki"
]
},
"name": "سرائیکی",
"400-page-title": "غلط ارداس",
"400-page-heading": "غلط ارداس",
"404-page-title": "مواد کائنی لبھیا",

View File

@@ -6,6 +6,7 @@
"Rofiatmustapha12"
]
},
"name": "slovenščina",
"suggest-full-text-search": "vsebuje »{{{SEARCH_TERMS}}}« ...",
"no-such-book": "Ni take knjige: {{BOOK_NAME}}",
"too-many-books": "Preveč zahtevanih knjig ({{NB_BOOKS}}), omejitev je {{LIMIT}}",
@@ -22,25 +23,9 @@
"400-page-heading": "Neveljaven zahtevek",
"404-page-title": "Vsebine ni mogoče najti",
"404-page-heading": "Ni najdeno",
"new-404-page-title": "Stran ni bila najdena",
"new-404-page-heading": "Ups. Stran ni bila najdena.",
"404-img-text": "Ni najdeno!",
"path-was-not-found": "Zahtevana pot ni bila najdena:",
"404-advice.p1": "Vsebina, ki jo iščete, je mogoče še vedno na voljo, vendar se morda nahaja na drugem mestu v datoteki ZIM.",
"404-advice.p2": "Prosimo:",
"404-advice.p3": "Poskusite uporabiti funkcijo iskanja, da najdete želeno vsebino",
"404-advice.p4": "Poiščite ključne besede ali naslove, povezane z informacijami, ki jih iščete",
"404-advice.p5": "Ta pristop bi vam moral pomagati najti želeno vsebino, tudi če izvirna povezava ne deluje pravilno.",
"500-page-title": "Notranja napaka strežnika",
"500-page-heading": "Ups. Stran ne deluje.",
"500-page-text": "Zahtevane poti ni mogoče pravilno dostaviti:",
"500-img-text": "Stran ne deluje",
"external-link-detected": "Zaznana zunanja povezava",
"caution-warning": "Pozor!",
"external-link-intro": "Zapustili boste bralnik ZIM v Kiwixu in se v internetu povezali z",
"external-link-advice.p1": "Povezava, do katere poskušate dostopati, ni del vašega paketa za delo brez povezave in zahteva internetno povezavo.",
"external-link-advice.p2": "Če se lahko povežete z internetom, lahko poskusite odpreti povezavo.",
"external-link-advice.p3": "Na vsebino ZIM brez povezave se lahko vrnete tudi z gumbom za nazaj v brskalniku.",
"500-page-heading": "Notranja napaka strežnika",
"500-page-text": "Prišlo je do notranje napake strežnika. Žal nam je za to. :/",
"fulltext-search-unavailable": "Iskanje po celotnem besedilu ni na voljo",
"no-search-results": "Iskalnik po celotnem besedilu za to vsebino ni na voljo.",
"search-results-page-title": "Iskanje: {{SEARCH_PATTERN}}",
@@ -48,10 +33,10 @@
"empty-search-results-page-header": "Ni zadetkov za »<b>{{{SEARCH_PATTERN}}}</b>«",
"search-result-book-info": "iz {{BOOK_TITLE}}",
"word-count": "{{COUNT}} besed",
"library-button-text": "Pojdi na pozdravno stran",
"home-button-text": "Pojdi na glavno stran »{{{BOOK_TITLE}}}«",
"random-page-button-text": "Pojdi na naključno izbrano stran",
"searchbox-tooltip": "Poiščite »{{{BOOK_TITLE}}}«",
"library-button-text": "Pojdite na pozdravno stran",
"home-button-text": "Pojdite na glavno stran »{{BOOK_TITLE}}«",
"random-page-button-text": "Pojdite na naključno izbrano stran",
"searchbox-tooltip": "Poiščite »{{BOOK_TITLE}}«",
"confusion-of-tongues": "V iskanju bi bili uporabljeni dve ali več knjig v različnih jezikih, kar lahko pripelje do nejasnih zadetkov.",
"welcome-page-overzealous-filter": "Ni zadetkov. Želite <a href=\"{{URL}}\">ponastaviti filter</a>?",
"powered-by-kiwix-html": "Omogoča <a href=\"https://kiwix.org\">Kiwix</a>",
@@ -61,31 +46,20 @@
"count-of-matching-books": "{{COUNT}} knjiga(i/e)",
"download": "Prenesi",
"direct-download-link-text": "Neposredno",
"direct-download-alt-text": "Neposredni prenos prek HTTP(S)",
"hash-download-link-text": "Kontrolna vsota SHA-256",
"hash-download-alt-text": "Prikaz kontrolne vsote SHA-256 datoteke",
"direct-download-alt-text": "neposredni prenos",
"hash-download-link-text": "Zgoščena vrednost SHA256",
"hash-download-alt-text": "prenesi zgoščeno vrednost",
"magnet-link-text": "Magnetna povezava",
"magnet-alt-text": "Prenos prek magnetne povezave",
"torrent-download-link-text": "BitTorrent",
"torrent-download-alt-text": "Prenos prek BitTorrenta",
"magnet-alt-text": "prenesi magnet",
"torrent-download-link-text": "Torrent datoteka",
"torrent-download-alt-text": "prenesi torrent",
"library-opds-feed-all-entries": "Knjižnični vir OPDS Vsi vnosi",
"filter-by-tag": "Filtriraj po oznaki »{{{TAG}}}«",
"stop-filtering-by-tag": "Ustavi filtriranje po oznaki »{{{TAG}}}«",
"filter-by-tag": "Filtriraj po oznaki »{{TAG}}«",
"stop-filtering-by-tag": "Ustavi filtriranje po oznaki »{{TAG}}«",
"library-opds-feed-parameterised": "Knjižnični vir OPDS vnosi, ki se ujemajo z {{#LANG}}\nJezik: {{LANG}} {{/LANG}}{{#CATEGORY}}\nKategorija: {{CATEGORY}} {{/CATEGORY}} {{#TAG}}\nOznaka: {{TAG}} {{/TAG}}{{#Q}}\nPoizvedba: {{Q}} {{/Q}}",
"welcome-to-kiwix-server": "Pozdravljeni na strežniku Kiwix",
"download-links-heading": "Povezave za prenos za <b><i>{{BOOK_TITLE}}</i></b>",
"download-links-title": "Prenesi knjigo",
"preview-book": "Predogled",
"unknown-error": "Neznana napaka",
"book-category.wikibooks": "Wikiknjige",
"book-category.wikinews": "Wikinovice",
"book-category.wikipedia": "Wikipedija",
"book-category.wikiquote": "Wikinavedek",
"book-category.wikisource": "Wikivir",
"book-category.wikispecies": "Wikivrste",
"book-category.wikiversity": "Wikiverza",
"book-category.wikivoyage": "Wikipotovanje",
"book-category.wiktionary": "Wikislovar",
"book-category.other": "Drugo",
"text-loading-content": "Nalaganje vsebine"
"unknown-error": "Neznana napaka"
}

View File

@@ -4,6 +4,7 @@
"Besnik b"
]
},
"name": "Shqip",
"suggest-full-text-search": "që përmban '{{{SEARCH_TERMS}}}'…",
"no-such-book": "Ska libër të tillë: {{BOOK_NAME}}",
"too-many-books": "U kërkuan shumë libra ({{NB_BOOKS}}), teksa kufiri është {{LIMIT}}",

View File

@@ -8,6 +8,7 @@
"WikiPhoenix"
]
},
"name": "Svenska",
"suggest-full-text-search": "innehåller '{{{SEARCH_TERMS}}}'...",
"no-such-book": "Ingen sådan bok: {{BOOK_NAME}}",
"too-many-books": "För många böcker begärda ({{NB_BOOKS}}) där gränsen är {{LIMIT}}",

View File

@@ -7,6 +7,7 @@
"Wangombe"
]
},
"name": "Kiswahili",
"suggest-full-text-search": "ina '{{{SEARCH_TERMS}}}}'...",
"no-such-book": "Hakuna kitabu kama hiki: {{BOOK_NAME}}",
"too-many-books": "Vitabu vingi mno vimeombwa ({{NB_BOOKS}}) ambapo kikomo ni {{LIMIT}}",

View File

@@ -6,6 +6,7 @@
"V Bhavya"
]
},
"name": "ఇంగ్లీషు",
"suggest-full-text-search": "'{{{SEARCH_TERMS}}}'ని కలిగి ఉంది...",
"no-such-book": "అలాంటి పుస్తకం లేదు: {{BOOK_NAME}}",
"too-many-books": "పరిమితి {{LIMIT}} ఉన్న చాలా పుస్తకాలు అభ్యర్థించబడ్డాయి ({{NB_BOOKS}})",

View File

@@ -4,6 +4,7 @@
"Kareyac"
]
},
"name": "Fake language for i18n testing"
, "suggest-full-text-search": "[I18N TESTING] cOnTaInInG '{{{SEARCH_TERMS}}}'..."
, "no-such-book": "[I18N TESTING] No such book: {{BOOK_NAME}}. Sorry."
, "url-not-found": "[I18N TESTING] URL not found: {{url}}"
@@ -21,10 +22,6 @@
, "404-advice.p3": "[I18N TESTING] Check the spelling of the URL path"
, "404-advice.p4": "[I18N TESTING] Press the dice button"
, "404-advice.p5": "Good luck! [I18N TESTING]"
, "500-page-title" : "[I18N] Internal Server Error [TESTING]"
, "500-page-heading" : "Oops. [I18N] Page isn't [TESTING] working."
, "500-page-text": "The requested path [I18N TESTING] cannot be properly delivered:"
, "500-img-text": "Page [I18N] isn't [TESTING] working"
, "external-link-detected" : "External [I18] Link [TESTING] Detected"
, "caution-warning" : "[I18N] C5n! [TESTING]"
, "external-link-intro" : "[I18N TESTING] The following link may lead you to a place from which you won't ever be able to return"

View File

@@ -5,6 +5,7 @@
"Rofiatmustapha12"
]
},
"name": "Türkçe",
"suggest-full-text-search": "'{{{SEARCH_TERMS}}}' içeriyor...",
"no-such-book": "Böyle bir kitap yok: {{BOOK_NAME}}",
"too-many-books": "Sınır {{LIMIT}} olduğunda çok fazla ({{NB_BOOKS}}) kitap istendi",

View File

@@ -4,7 +4,6 @@
"GuoPC",
"IceButBin",
"Kichin",
"Peterxy12",
"Prmsh",
"StarrySky",
"Sunai",
@@ -12,6 +11,7 @@
"沈澄心"
]
},
"name": "简体中文",
"suggest-full-text-search": "正在查找「{{{SEARCH_TERMS}}}」…",
"no-such-book": "没有名为“{{BOOK_NAME}}”的图书",
"too-many-books": "请求的图书过多 ({{NB_BOOKS}}),上限为 {{LIMIT}}",
@@ -39,9 +39,9 @@
"search-result-book-info": "来自{{BOOK_TITLE}}",
"word-count": "{{COUNT}} 个字",
"library-button-text": "转到欢迎页面",
"home-button-text": "转到“{{{BOOK_TITLE}}}”的主页",
"home-button-text": "转到“{{BOOK_TITLE}}”的主页",
"random-page-button-text": "前往随机选择的页面",
"searchbox-tooltip": "搜索“{{{BOOK_TITLE}}}”",
"searchbox-tooltip": "搜索“{{BOOK_TITLE}}”",
"confusion-of-tongues": "两本或多本不同语言的图书将同时被搜索,这可能会导致搜索结果混乱。",
"welcome-page-overzealous-filter": "没有结果。您想<a href=\"{{URL}}\">重置过滤器</a>吗?",
"powered-by-kiwix-html": "由<a href=\"https://kiwix.org\">Kiwix</a>提供技术支持",

View File

@@ -6,6 +6,7 @@
"Winston Sung"
]
},
"name": "繁體中文",
"suggest-full-text-search": "正在包含「{{{SEARCH_TERMS}}}」…",
"no-such-book": "沒有這樣的書籍:{{BOOK_NAME}}",
"too-many-books": "請求太多個書籍({{NB_BOOKS}}),上限是 {{LIMIT}} 個",
@@ -22,25 +23,9 @@
"400-page-heading": "無效請求",
"404-page-title": "查無內容",
"404-page-heading": "查無頁面",
"new-404-page-title": "找不到頁面",
"new-404-page-heading": "哎呀,找不到頁面。",
"404-img-text": "找不到!",
"path-was-not-found": "找不到請求路徑。",
"404-advice.p1": "您正在尋找的內容可能仍然可用,只是可能位於 ZIM 檔案中的不同位置。",
"404-advice.p2": "請:",
"404-advice.p3": "嘗試使用搜尋功能來尋找您想要的內容",
"404-advice.p4": "查看與您正在尋找資訊相關的關鍵字或標題",
"404-advice.p5": "即使原始連結無法正常運作,這種方法應該可以幫助您發現到所需的內容。",
"500-page-title": "內部伺服器錯誤",
"500-page-heading": "哎呀,頁面無法正常運作。",
"500-page-text": "請求的路徑無法正確傳遞:",
"500-img-text": "頁面無法正常運作",
"external-link-detected": "偵測到外部連結",
"caution-warning": "注意!",
"external-link-intro": "您即將離開 Kiwix 的 ZIM 閱讀器,並上網到",
"external-link-advice.p1": "您嘗試存取的連結不在離線套件裡,需要網路連線。",
"external-link-advice.p2": "如果您可以連上網路,您可以嘗試開啟該連結。",
"external-link-advice.p3": "您也可以使用瀏覽器的返回按鈕跳回到 ZIM 離線內容。",
"500-page-heading": "內部伺服器錯誤",
"500-page-text": "內部伺服器發生錯誤。對此我們深感抱歉:/",
"fulltext-search-unavailable": "全文搜尋無效",
"no-search-results": "全文搜尋引擎不適用此內容。",
"search-results-page-title": "搜尋:{{SEARCH_PATTERN}}",
@@ -49,9 +34,9 @@
"search-result-book-info": "來自{{BOOK_TITLE}}",
"word-count": "{{COUNT}}個字",
"library-button-text": "前往歡迎首頁",
"home-button-text": "前往「{{{BOOK_TITLE}}}」的首頁",
"home-button-text": "前往「{{BOOK_TITLE}}」的首頁",
"random-page-button-text": "前往隨機選取頁面",
"searchbox-tooltip": "在「{{{BOOK_TITLE}}}」搜尋",
"searchbox-tooltip": "在{{BOOK_TITLE}}搜尋",
"confusion-of-tongues": "搜索裡有加入兩本或更多不同語言的書籍,這可能會導致混淆結果。",
"welcome-page-overzealous-filter": "沒有結果。您想要<a href=\"{{URL}}\">重新設定篩選</a>嗎?",
"powered-by-kiwix-html": "由 <a href=\"https://kiwix.org\">Kiwix</a> 提供技術支援",
@@ -68,10 +53,10 @@
"magnet-alt-text": "透過磁力連結下載",
"torrent-download-link-text": "BitTorrent",
"torrent-download-alt-text": "透過 BitTorrent 下載",
"library-opds-feed-all-entries": "文庫 OPDS 摘要 - 所有項目",
"library-opds-feed-all-entries": "圖書館 OPDS 摘要 - 所有項目",
"filter-by-tag": "依標籤「{{{TAG}}}」篩選",
"stop-filtering-by-tag": "停止依標籤「{{{TAG}}}」篩選",
"library-opds-feed-parameterised": "文庫 OPDS 摘要 - 項目符合 {{#LANG}}\n語言{{LANG}} {{/LANG}}{{#CATEGORY}}\n分類{{CATEGORY}} {{/CATEGORY}}{{#TAG}}\n標籤{{TAG}} {{/TAG}}{{#Q}}\n查詢{{Q}} {{/Q}}",
"library-opds-feed-parameterised": "圖書館 OPDS 摘要 - 項目符合 {{#LANG}}\n語言{{LANG}} {{/LANG}}{{#CATEGORY}}\n分類{{CATEGORY}} {{/CATEGORY}}{{#TAG}}\n標籤{{TAG}} {{/TAG}}{{#Q}}\n查詢{{Q}} {{/Q}}",
"welcome-to-kiwix-server": "歡迎來到 Kiwix 伺服器",
"download-links-heading": "下載<b><i>{{BOOK_TITLE}}</i></b>的連結",
"download-links-title": "下載書籍",

View File

@@ -152,21 +152,8 @@
: '';
}
function addBookPreviewLink(html, bookXml) {
const bookContentLink = bookXml.querySelector('link[type="text/html"]');
if ( !bookContentLink )
return html;
const urlComponents = bookContentLink.getAttribute('href').split('/');
// bookContentLink URL = ROOT_URL/content/BOOK_NAME
const bookName = urlComponents.pop();
urlComponents.pop(); // drop 'content' component
const viewerLink = urlComponents.join('/') + `/viewer#${bookName}`;
return `<a class="book__link" href="${viewerLink}" data-hover="Preview">${html}</a>`;
}
function generateBookHtml(book, sort = false) {
const link = book.querySelector('link[type="text/html"]').getAttribute('href');
let iconUrl;
book.querySelectorAll('link[rel="http://opds-spec.org/image/thumbnail"]').forEach(link => {
if (link.getAttribute('type').split(';')[1] == 'width=48' && !iconUrl) {
@@ -196,6 +183,9 @@
} catch {
downloadLink = '';
}
const bookName = link.split('/').pop();
const viewerLink = `${root}/viewer#${bookName}`;
const humanFriendlyZimSize = humanFriendlySize(zimSize);
const divTag = document.createElement('div');
@@ -207,7 +197,9 @@
const faviconAttr = iconUrl != undefined ? `style="background-image: url('${iconUrl}')"` : '';
const languageAttr = langCode != '' ? `title="${language}" aria-label="${language}"` : 'style="background-color: transparent"';
let bookLinkWrapper = `
divTag.innerHTML = `
<div class="book__wrapper">
<a class="book__link" href="${viewerLink}" data-hover="Preview">
<div class="book__link__wrapper">
<div class="book__icon" ${faviconAttr}></div>
<div class="book__header">
@@ -215,11 +207,7 @@
</div>
<div class="book__description" title="${description}">${description}</div>
</div>
`;
divTag.innerHTML = `
<div class="book__wrapper">
${addBookPreviewLink(bookLinkWrapper, book)}
</a>
<div class="book__meta">
<div class="book__languageTag" ${languageAttr}>${getLanguageCodeToDisplay(langCode)}</div>
<div class="book__tags"><div class="book__tags--wrapper">${tagHtml}</div></div>

View File

@@ -13,7 +13,6 @@ html {
body {
position: relative;
box-sizing: border-box;
background: #ffffff;
}
.loader {
@@ -102,10 +101,6 @@ body {
padding: 20px;
}
#content_iframe {
background: #ffffff;
}
#uiLanguageSelector {
display: none;
}

View File

@@ -2,12 +2,12 @@ const uiLanguages = [
{
"iso_code": "ar",
"self_name": "الإنجليزية",
"translation_count": 44
"translation_count": 43
},
{
"iso_code": "bn",
"self_name": "বাংলা",
"translation_count": 34
"translation_count": 24
},
{
"iso_code": "br",
@@ -37,7 +37,7 @@ const uiLanguages = [
{
"iso_code": "en",
"self_name": "English",
"translation_count": 93
"translation_count": 77
},
{
"iso_code": "es",
@@ -52,7 +52,7 @@ const uiLanguages = [
{
"iso_code": "fr",
"self_name": "Français",
"translation_count": 84
"translation_count": 68
},
{
"iso_code": "ha",
@@ -62,27 +62,22 @@ const uiLanguages = [
{
"iso_code": "he",
"self_name": "עברית",
"translation_count": 80
"translation_count": 69
},
{
"iso_code": "hi",
"self_name": "हिन्दी",
"translation_count": 59
},
{
"iso_code": "hu",
"self_name": "Magyar",
"translation_count": 32
},
{
"iso_code": "hy",
"self_name": "Հայերեն",
"translation_count": 26
"translation_count": 25
},
{
"iso_code": "ia",
"self_name": "interlingua",
"translation_count": 84
"translation_count": 67
},
{
"iso_code": "id",
@@ -97,7 +92,7 @@ const uiLanguages = [
{
"iso_code": "it",
"self_name": "italiano",
"translation_count": 58
"translation_count": 54
},
{
"iso_code": "ja",
@@ -107,7 +102,7 @@ const uiLanguages = [
{
"iso_code": "ko",
"self_name": "한국어",
"translation_count": 73
"translation_count": 64
},
{
"iso_code": "ku-latn",
@@ -117,12 +112,12 @@ const uiLanguages = [
{
"iso_code": "lb",
"self_name": "Lëtzebuergesch",
"translation_count": 48
"translation_count": 43
},
{
"iso_code": "mk",
"self_name": "македонски",
"translation_count": 81
"translation_count": 76
},
{
"iso_code": "ms",
@@ -132,7 +127,7 @@ const uiLanguages = [
{
"iso_code": "nb",
"self_name": "Engelsk",
"translation_count": 68
"translation_count": 50
},
{
"iso_code": "nl",
@@ -157,7 +152,7 @@ const uiLanguages = [
{
"iso_code": "pt-br",
"self_name": "Português",
"translation_count": 66
"translation_count": 65
},
{
"iso_code": "pt",
@@ -167,7 +162,7 @@ const uiLanguages = [
{
"iso_code": "ro",
"self_name": "Engleză",
"translation_count": 68
"translation_count": 67
},
{
"iso_code": "ru",
@@ -187,7 +182,7 @@ const uiLanguages = [
{
"iso_code": "skr-arab",
"self_name": "سرائیکی",
"translation_count": 33
"translation_count": 31
},
{
"iso_code": "sl",
@@ -227,6 +222,6 @@ const uiLanguages = [
{
"iso_code": "zh-hant",
"self_name": "繁體中文",
"translation_count": 92
"translation_count": 76
}
]

View File

@@ -1,3 +0,0 @@
#kiwixtoolbarwrapper {
display: none !important;
}

View File

@@ -34,12 +34,6 @@
width: 20em;
}
#kiwixsearchbox {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
#kiwix_serve_taskbar_home_button button {
overflow: hidden;
text-overflow: ellipsis;

View File

@@ -10,8 +10,6 @@ let viewerState = {
uiLanguage: 'en',
};
const FULL_ROOT_URL = `${window.location.origin}${root}`;
function dropUserLang(query) {
const q = new URLSearchParams(query);
q.delete('userlang');
@@ -104,7 +102,7 @@ function suggestionsApiURL()
{
const uriEncodedBookName = encodeURIComponent(currentBook);
const userLang = viewerState.uiLanguage;
return `${root}/suggest?userlang=${userLang}&content=${uriEncodedBookName}`;
return `${root}/suggest?userlang=${userLang}&mode=smart&content=${uriEncodedBookName}`;
}
function setTitle(element, text) {
@@ -188,9 +186,13 @@ function closeSuggestions() {
}
}
function setSearchQuery(text) {
document.getElementById("kiwixsearchbox").value = text;
}
function updateSearchBoxForLocationChange() {
closeSuggestions();
document.getElementById("kiwixsearchbox").value = getSearchPattern();
setSearchQuery(getSearchPattern());
}
function updateSearchBoxForBookChange() {
@@ -267,18 +269,11 @@ function translateErrorPageIfNeeded() {
translatePageInWindow(contentIframe.contentWindow);
}
let iframeLocationHref = null;
function handle_content_url_change() {
if ( iframeLocationHref == contentIframe.contentWindow.location.href )
return;
const iframeLocation = contentIframe.contentWindow.location;
iframeLocationHref = iframeLocation.href;
console.log('handle_content_url_change: ' + iframeLocation.href);
document.title = contentIframe.contentDocument.title;
const iframeContentUrl = iframeLocation.pathname + iframeLocation.hash;
const iframeContentUrl = iframeLocation.pathname;
const iframeContentQuery = iframeLocation.search;
const newHash = iframeUrl2UserUrl(iframeContentUrl, iframeContentQuery);
history.replaceState(viewerState, null, makeURL(location.search, newHash));
@@ -356,12 +351,6 @@ function onClickEvent(e) {
if (target !== null && "href" in target) {
const target_href = getRealHref(target);
const target_url = new URL(target_href, iframeDocument.location);
if ( target_url.href.startsWith(`${FULL_ROOT_URL}/viewer#`) &&
!linkShouldBeOpenedInANewWindow(target, e) ) {
contentIframe.contentWindow.parent.location = target_url;
e.preventDefault();
return;
}
const isExternalAppUrl = urlMustBeHandledByAnExternalApp(target_url);
if ( (isExternalAppUrl && !viewerSettings.linkBlockingEnabled)
|| goingToOpenALinkToAnUndisplayableResource(target_url) ) {
@@ -413,24 +402,14 @@ this.Element && function(ElementPrototype) {
}
}(Element.prototype);
function setup_external_link_blocker() {
setupEventHandler(contentIframe.contentDocument, 'a', 'click', onClickEvent);
}
////////////////////////////////////////////////////////////////////////////////
// End of external link blocking
////////////////////////////////////////////////////////////////////////////////
const internalUrlPrefix = `${FULL_ROOT_URL}/content/`;
function setup_chaperon_mode() {
setupEventHandler(contentIframe.contentDocument, 'a', 'click', onClickEvent);
const links = contentIframe.contentDocument.getElementsByTagName('a');
for ( const a of links ) {
// XXX: wombat's possible involvement with href not taken into account
if ( a.hasAttribute('href') && a.href.startsWith(internalUrlPrefix) ) {
const userUrl = a.href.substr(internalUrlPrefix.length);
a.href = `${root}/viewer#${userUrl}`;
}
}
}
let viewerSetupComplete = false;
function on_content_load() {
@@ -438,9 +417,10 @@ function on_content_load() {
contentIframe.classList.remove("hidden");
loader.style.display = "none";
contentIframe.contentWindow.onhashchange = handle_content_url_change;
setInterval(handle_content_url_change, 100);
setup_chaperon_mode();
if ( viewerSetupComplete ) {
handle_content_url_change();
}
setup_external_link_blocker();
}
function htmlDecode(input) {
@@ -488,7 +468,8 @@ function setupSuggestions() {
resultItem: {
element: (item, data) => {
const uriEncodedBookName = encodeURIComponent(currentBook);
let url;
const linkText = htmlDecode(data.value.label);
let url, modifiedQuery;
if (data.value.kind == "path") {
// The double quote and backslash symbols are included in the list
// of special symbols to URI-encode so that the resulting URL can
@@ -497,15 +478,18 @@ function setupSuggestions() {
const path = htmlDecode(data.value.path);
const quasiUriEncodedPath = quasiUriEncode(path, '#?"\\');
url = `/content/${uriEncodedBookName}/${quasiUriEncodedPath}`;
} else {
} else if (data.value.kind == "pattern") {
const pattern = encodeURIComponent(htmlDecode(data.value.value));
url = `/search?content=${uriEncodedBookName}&pattern=${pattern}`;
} else { // data.value.kind == "modifiedquery"
modifiedQuery = htmlDecode(linkText);
}
// url can't contain any double quote and/or backslash symbols
// since they should have been URI-encoded. Therefore putting it
// inside double quotes should result in valid javascript.
const jsAction = `gotoUrl("${url}")`;
const linkText = htmlDecode(data.value.label);
const jsAction = url
? `gotoUrl("${url}")`
: `setSearchQuery("${modifiedQuery}")`;
item.innerHTML = makeJSLink(jsAction, linkText, 'class="suggest"');
},
highlight: "autoComplete_highlight",

View File

@@ -13,8 +13,7 @@
{{#icons}}<link rel="http://opds-spec.org/image/thumbnail"
href="{{root}}/catalog/v2/illustration/{{{id}}}/?size={{icon_size}}"
type="{{icon_mimetype}};width={{icon_size}};height={{icon_size}};scale=1"/>
{{/icons}}{{#contentAccessUrl}}<link type="text/html" href="{{contentAccessUrl}}/{{{content_id}}}" />
{{/contentAccessUrl}}
{{/icons}}<link type="text/html" href="{{root}}/content/{{{content_id}}}" />
<author>
<name>{{author_name}}</name>
</author>

View File

@@ -33,6 +33,7 @@
<meta name="msapplication-config" content="{{root}}/skin/favicon/browserconfig.xml?KIWIXCACHEID">
<meta name="theme-color" content="#ffffff">
<script type="text/javascript" src="./skin/polyfills.js?KIWIXCACHEID"></script>
<script type="text/javascript" src="./viewer_settings.js"></script>
<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>

View File

@@ -3,6 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link type="root" href="{{root}}">
<title>{{translations.welcome-to-kiwix-server}}</title>
<link
type="text/css"
@@ -107,7 +108,7 @@
<h3 class="kiwixHomeBody__results">{{translations.count-of-matching-books}}</h3>
{{#books}}
<div class="book__wrapper">
{{#contentAccessUrl}}<a class="book__link" href="{{contentAccessUrl}}/{{id}}" title="{{translations.preview-book}}" aria-label="{{translations.preview-book}}">{{/contentAccessUrl}}
<a class="book__link" href="{{root}}/content/{{id}}" title="{{translations.preview-book}}" aria-label="{{translations.preview-book}}">
<div class="book__link__wrapper">
<div class="book__icon" {{faviconAttr}}></div>
<div class="book__header">
@@ -115,7 +116,7 @@
</div>
<div class="book__description" title="{{description}}">{{description}}</div>
</div>
{{#contentAccessUrl}}</a>{{/contentAccessUrl}}
</a>
<div class="book__meta">
<div class="book__languageTag" title="{{langTag.langFullString}}" aria-label="{{langTag.langFullString}}">{{langTag.langShortString}}</div>
<div class="book__tags"><div class="book__tags--wrapper">

View File

@@ -120,7 +120,7 @@
<cite>{{>snippet}}...</cite>
{{/snippet}}
{{#bookInfo}}
<div class="book-title">{{{bookInfo}}}</div>
<div class="book-title">{{bookInfo}}</div>
{{/bookInfo}}
{{#wordCountInfo}}
<div class="informations">{{wordCountInfo}}</div>

View File

@@ -1,31 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>{{PAGE_TITLE}}</title>
<link type="text/css" href="{{root}}/skin/error.css?KIWIXCACHEID" rel="Stylesheet" />
<script>
window.KIWIX_RESPONSE_TEMPLATE = "{{KIWIX_RESPONSE_TEMPLATE}}";
window.KIWIX_RESPONSE_DATA = {{{KIWIX_RESPONSE_DATA}}};
</script>
</head>
<body>
<header>
<img src="{{root}}/skin/500.svg?KIWIXCACHEID"
alt="{{500_img_text}}"
aria-label="{{500_img_text}}"
title="{{500_img_text}}">
</header>
<section class="intro">
<h1>{{PAGE_HEADING}}</h1>
<p>{{PAGE_TEXT}}</p>
<p><code>{{url_path}}</code></p>
</section>
{{#error}}
<section class="advice">
<p>{{error}}</p>
</section>
{{/error}}
</body>
</html>

View File

@@ -11,7 +11,6 @@
<link type="text/css" href="./skin/kiwix.css?KIWIXCACHEID" rel="Stylesheet" />
<link type="text/css" href="./skin/taskbar.css?KIWIXCACHEID" rel="Stylesheet" />
<link type="text/css" href="./skin/autoComplete/css/autoComplete.css?KIWIXCACHEID" rel="Stylesheet" />
<link type="text/css" href="./skin/print.css?KIWIXCACHEID" media="print" rel="Stylesheet" />
<script type="text/javascript" src="./skin/polyfills.js?KIWIXCACHEID"></script>
<script type="text/javascript" src="./viewer_settings.js"></script>
<script type="module" src="./skin/i18n.js?KIWIXCACHEID" defer></script>

View File

@@ -1,143 +0,0 @@
#!/usr/bin/env bash
mydir=$(readlink -f "$(dirname "$0")")
myname=$(basename "$0")
cd "$mydir"
zimfilename='spelling_correction_test.zim'
rm -f "$zimfilename"
datadir=$(mktemp -d --tmpdir $myname.XXXXXX)
function cleanup()
{
rm -rf "$datadir"
}
trap cleanup EXIT SIGINT SIGQUIT SIGHUP SIGTERM
generate_html_file()
{
local word="$1"
local letter_count=${#word}
local letters=""
local i
for (( i=0; i<letter_count; ++i ));
do
local l=${word:i:1}
if (( i == letter_count - 1 ))
then
letters+="and '$l'"
else
letters+="'$l', "
fi
done
cat >"$word".html <<END
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>$word</title>
</head>
<body>
<p>'$word' is a word consisting of the letters $letters.</p>
</body>
</html>
END
}
generate_zim_file_data()
{
local titles=(
"Abenteuer"
"Applaus"
"Assistent"
"Attacke"
"Bewandtnis"
"Biene"
"Botschafter"
"Chaos"
"Entgelt"
"Entzündung"
"Fahrradschloss"
"Führerschein"
"Gral"
"Hierarchie"
"Honig"
"Impfung"
"Kamera"
"Konkurrenz"
"Lachs"
"Mond"
"Pflaster"
"Phänomen"
"Prise"
"Schirmmütze"
"Sohn"
"Stuhl"
"Teller"
"Thermoskanne"
"Trog"
"Umweltstandard"
"Unfug"
"Wohnzimmer"
"Zunge"
"aber"
"abonnieren"
"amtieren"
"attestieren"
"ausleeren"
"beißen"
"ebenfalls"
"enttäuschen"
"fort"
"gefleckt"
"gefährlich"
"gestern"
"gewähren"
"hässlich"
"konkurrieren"
"kämmen"
"lustig"
"müssen"
"nämlich"
"runterfallen"
"sanft"
"schubsen"
"seit"
"vorgestern"
"wahrscheinlich"
"Willkommen"
# Entries for demonstrating shortcomings of the PoC implementation
"Lorem ipsum"
"King"
"Kong"
)
local t
(
cd "$datadir"
cp "$mydir"/../../static/skin/favicon/favicon-32x32.png favicon.png
for t in "${titles[@]}";
do
generate_html_file "$t"
done
)
}
generate_zim_file_data
zimwriterfs --withoutFTIndex --dont-check-arguments \
-w Willkommen.html \
-I favicon.png \
-l deu \
-n spelling_correction_test \
-t "Spelling corrections test" \
-d "ZIM file for testing spelling corrections" \
-c "Kiwix" \
-p "Kiwix" \
$datadir \
"$zimfilename" \
&& echo "$zimfilename was successfully created" \
|| echo '!!! Failed to create' "$zimfilename" '!!!' >&2

View File

@@ -51,21 +51,4 @@
size="556"
favicon="faviconMimeType_attribute_is_absent"
></book>
<book
id="inaccessiblezim"
path="./nosuchzimfile.zim"
url="https://github.com/kiwix/libkiwix/raw/master/test/data/nosuchzimfile.zim"
title="Catalog of all catalogs"
description="Testing that running kiwix-serve without access to ZIM files doesn't lead to a catastrophe"
language="cat"
creator="Catherine of Catalonia"
publisher="Caterpillar"
date="2025-09-04"
name="catalog_of_all_catalogs"
tags="unittest;_category:cats"
articleCount="12107"
mediaCount="8"
size="20250904"
favicon="Catania Cathedral"
></book>
</library>

View File

Binary file not shown.

View File

@@ -18,15 +18,9 @@ protected:
const int PORT = 8002;
protected:
void resetServer(ZimFileServer::Cfg cfg) {
void resetServer(ZimFileServer::Options options) {
zfs1_.reset();
zfs1_.reset(new ZimFileServer(PORT, cfg, "./test/library.xml"));
}
void resetServer(ZimFileServer::Options options, std::string contentServerUrl="") {
ZimFileServer::Cfg cfg(options);
cfg.contentServerUrl = contentServerUrl;
resetServer(cfg);
zfs1_.reset(new ZimFileServer(PORT, options, "./test/library.xml"));
}
void SetUp() override {
@@ -156,30 +150,6 @@ std::string maskVariableOPDSFeedData(std::string s)
"125952"\
)
#define INACCESSIBLEZIMFILE_CATALOG_ENTRY \
" <entry>\n" \
" <id>urn:uuid:inaccessiblezim</id>\n" \
" <title>Catalog of all catalogs</title>\n" \
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n" \
" <summary>Testing that running kiwix-serve without access to ZIM files doesn&apos;t lead to a catastrophe</summary>\n" \
" <language>cat</language>\n" \
" <name>catalog_of_all_catalogs</name>\n" \
" <flavour></flavour>\n" \
" <category>cats</category>\n" \
" <tags>unittest;_category:cats</tags>\n" \
" <articleCount>12107</articleCount>\n" \
" <mediaCount>8</mediaCount>\n" \
" <link type=\"text/html\" href=\"/ROOT%23%3F/content/nosuchzimfile\" />\n" \
" <author>\n" \
" <name>Catherine of Catalonia</name>\n" \
" </author>\n" \
" <publisher>\n" \
" <name>Caterpillar</name>\n" \
" </publisher>\n" \
" <dc:issued>2025-09-04T00:00:00Z</dc:issued>\n" \
" <link rel=\"http://opds-spec.org/acquisition/open-access\" type=\"application/x-zim\" href=\"https://github.com/kiwix/libkiwix/raw/master/test/data/nosuchzimfile.zim\" length=\"20736925696\" />\n" \
" </entry>\n"
TEST_F(LibraryServerTest, catalog_root_xml)
{
const auto r = zfs1_->GET("/ROOT%23%3F/catalog/root.xml");
@@ -590,15 +560,6 @@ TEST_F(LibraryServerTest, catalog_v2_categories)
<title>List of categories</title>
<updated>YYYY-MM-DDThh:mm:ssZ</updated>
<entry>
<title>cats</title>
<link rel="subsection"
href="/ROOT%23%3F/catalog/v2/entries?category=cats"
type="application/atom+xml;profile=opds-catalog;kind=acquisition"/>
<updated>YYYY-MM-DDThh:mm:ssZ</updated>
<id>12345678-90ab-cdef-1234-567890abcdef</id>
<content type="text">All entries with category of 'cats'.</content>
</entry>
<entry>
<title>jazz</title>
<link rel="subsection"
@@ -641,16 +602,6 @@ TEST_F(LibraryServerTest, catalog_v2_languages)
<title>List of languages</title>
<updated>YYYY-MM-DDThh:mm:ssZ</updated>
<entry>
<title>català</title>
<dc:language>cat</dc:language>
<thr:count>1</thr:count>
<link rel="subsection"
href="/ROOT%23%3F/catalog/v2/entries?lang=cat"
type="application/atom+xml;profile=opds-catalog;kind=acquisition"/>
<updated>YYYY-MM-DDThh:mm:ssZ</updated>
<id>12345678-90ab-cdef-1234-567890abcdef</id>
</entry>
<entry>
<title>English</title>
<dc:language>eng</dc:language>
@@ -727,49 +678,6 @@ TEST_F(LibraryServerTest, catalog_v2_entries)
);
}
TEST_F(LibraryServerTest, catalog_v2_entries_catalog_only_mode)
{
const std::string contentServerUrl = "https://demo.kiwix.org";
const auto fixContentLinks = [=](std::string s) -> std::string {
s = replace(s, "/ROOT%23%3F/content", contentServerUrl + "/content");
return s;
};
resetServer(ZimFileServer::CATALOG_ONLY_MODE, contentServerUrl);
const auto r = zfs1_->GET("/ROOT%23%3F/catalog/v2/entries");
EXPECT_EQ(r->status, 200);
const std::string expectedOPDS =
CATALOG_V2_ENTRIES_PREAMBLE("")
" <title>All Entries</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
"\n"
+ fixContentLinks(CHARLES_RAY_CATALOG_ENTRY)
+ fixContentLinks(INACCESSIBLEZIMFILE_CATALOG_ENTRY)
+ fixContentLinks(RAY_CHARLES_CATALOG_ENTRY)
+ fixContentLinks(UNCATEGORIZED_RAY_CHARLES_CATALOG_ENTRY) +
"</feed>\n";
EXPECT_EQ(maskVariableOPDSFeedData(r->body), expectedOPDS);
{ // test with empty rootLocation
const auto fixRoot = [=](std::string s) -> std::string {
s = replace(s, "/ROOT%23%3F/", "/");
s = replace(s, "/ROOT%23%3F/", "/");
return s;
};
ZimFileServer::Cfg serverCfg;
serverCfg.root = "";
serverCfg.options = ZimFileServer::CATALOG_ONLY_MODE;
serverCfg.contentServerUrl = contentServerUrl;
resetServer(serverCfg);
const auto r = zfs1_->GET("/catalog/v2/entries");
EXPECT_EQ(r->status, 200);
EXPECT_EQ(maskVariableOPDSFeedData(r->body), fixRoot(expectedOPDS));
}
}
TEST_F(LibraryServerTest, catalog_v2_entries_filtered_by_range)
{
{
@@ -1277,10 +1185,11 @@ TEST_F(LibraryServerTest, no_name_mapper_catalog_v2_individual_entry_access)
" <head>\n" \
" <meta charset=\"UTF-8\" />\n" \
" <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n" \
" <link type=\"root\" href=\"/ROOT%23%3F\">\n" \
" <title>Welcome to Kiwix Server</title>\n" \
" <link\n" \
" type=\"text/css\"\n" \
" href=\"/ROOT%23%3F/skin/kiwix.css?cacheid=b4e29e64\"\n" \
" href=\"/ROOT%23%3F/skin/kiwix.css?cacheid=3948b846\"\n" \
" rel=\"Stylesheet\"\n" \
" />\n" \
" <link\n" \
@@ -1411,32 +1320,6 @@ TEST_F(LibraryServerTest, no_name_mapper_catalog_v2_individual_entry_access)
" </div>\n" \
" </div>\n"
#define INACCESSIBLEZIMFILE_BOOK_HTML \
" <div class=\"book__wrapper\">\n" \
" <a class=\"book__link\" href=\"/ROOT%23%3F/content/nosuchzimfile\" title=\"Preview\" aria-label=\"Preview\">\n" \
" <div class=\"book__link__wrapper\">\n" \
" <div class=\"book__icon\" style=background-image:url(/ROOT%23%3F/catalog/v2/illustration/inaccessiblezim/?size=48)></div>\n" \
" <div class=\"book__header\">\n" \
" <div id=\"book__title\">Catalog of all catalogs</div>\n" \
" </div>\n" \
" <div class=\"book__description\" title=\"Testing that running kiwix-serve without access to ZIM files doesn&apos;t lead to a catastrophe\">Testing that running kiwix-serve without access to ZIM files doesn&apos;t lead to a catastrophe</div>\n" \
" </div>\n" \
" </a>\n" \
" <div class=\"book__meta\">\n" \
" <div class=\"book__languageTag\" title=\"català\" aria-label=\"català\">cat</div>\n" \
" <div class=\"book__tags\"><div class=\"book__tags--wrapper\">\n" \
" <span class=\"tag__link\" aria-label='unittest' title='unittest'>unittest</span>\n" \
" </div>\n" \
" </div>\n" \
" </div>\n" \
" <div>\n" \
" <a class=\"book__download\" href=\"/ROOT%23%3F/nojs/download/nosuchzimfile\">\n" \
" <img src=\"/ROOT%23%3F/skin/download-white.svg?cacheid=079ab989\">\n" \
" <span>Download</span>\n" \
" </a>\n" \
" </div>\n" \
" </div>\n"
#define FINAL_HTML_TEXT \
" </div>\n" \
" </div>\n" \
@@ -1449,7 +1332,6 @@ TEST_F(LibraryServerTest, no_name_mapper_catalog_v2_individual_entry_access)
" <div class=\"kiwixNav__select\">\n" \
" <select name=\"lang\" id=\"languageFilter\" class='kiwixNav__kiwixFilter filter' form=\"kiwixSearchForm\">\n" \
" <option value=\"\" selected>All languages</option>\n" \
" <option value=\"cat\">català</option>\n" \
" <option value=\"eng\"" SELECTED_ENG ">English</option>\n" \
" <option value=\"fra\">français</option>\n" \
" <option value=\"rus\">русский</option>\n" \
@@ -1458,7 +1340,6 @@ TEST_F(LibraryServerTest, no_name_mapper_catalog_v2_individual_entry_access)
" <div class=\"kiwixNav__select\">\n" \
" <select name=\"category\" id=\"categoryFilter\" class='kiwixNav__kiwixFilter filter' form=\"kiwixSearchForm\">\n" \
" <option value=\"\">All categories</option>\n" \
" <option value=\"cats\">Cats</option>\n" \
" <option value=\"jazz\">Jazz</option>\n" \
" <option value=\"wikipedia\">Wikipedia</option>\n" \
" </select>\n" \
@@ -1572,47 +1453,4 @@ TEST_F(LibraryServerTest, noJS) {
EXPECT_EQ(r->body, RAY_CHARLES_UNCTZ_DOWNLOAD);
}
TEST_F(LibraryServerTest, noJS_catalogOnlyMode) {
const std::string contentServerUrl = "https://demo.kiwix.org";
const auto fixContentLinks = [=](std::string s) -> std::string {
s = replace(s, "/ROOT%23%3F/content", contentServerUrl + "/content");
return s;
};
resetServer(ZimFileServer::CATALOG_ONLY_MODE, contentServerUrl);
auto r = zfs1_->GET("/ROOT%23%3F/nojs");
EXPECT_EQ(r->status, 200);
const std::string expectedHTML =
HTML_PREAMBLE
FILTERS_HTML("")
HOME_BODY_TEXT("4")
+ fixContentLinks(CHARLES_RAY_BOOK_HTML)
+ fixContentLinks(INACCESSIBLEZIMFILE_BOOK_HTML)
+ fixContentLinks(RAY_CHARLES_BOOK_HTML)
+ fixContentLinks(RAY_CHARLES_UNCTZ_BOOK_HTML)
+ FINAL_HTML_TEXT;
EXPECT_EQ(r->body, expectedHTML);
{ // test with empty rootLocation
const auto fixRoot = [=](std::string s) -> std::string {
s = replace(s, "/ROOT%23%3F/", "/");
return s;
};
ZimFileServer::Cfg serverCfg;
serverCfg.root = "";
serverCfg.options = ZimFileServer::CATALOG_ONLY_MODE;
serverCfg.contentServerUrl = contentServerUrl;
resetServer(serverCfg);
auto r = zfs1_->GET("/nojs");
EXPECT_EQ(r->status, 200);
EXPECT_EQ(r->body, fixRoot(expectedHTML));
}
}
#undef EXPECT_SEARCH_RESULTS

View File

@@ -88,7 +88,6 @@ TEST(Manager, reload)
manager.reload({ "./test/library.xml" });
EXPECT_EQ(lib->getBooksIds(), (kiwix::Library::BookIdCollection{
"charlesray",
"inaccessiblezim",
"raycharles",
"raycharles_uncategorized"
}));
@@ -96,14 +95,12 @@ TEST(Manager, reload)
lib->removeBookById("raycharles");
EXPECT_EQ(lib->getBooksIds(), (kiwix::Library::BookIdCollection{
"charlesray",
"inaccessiblezim",
"raycharles_uncategorized"
}));
manager.reload({ "./test/library.xml" });
EXPECT_EQ(lib->getBooksIds(), kiwix::Library::BookIdCollection({
"charlesray",
"inaccessiblezim",
"raycharles",
"raycharles_uncategorized"
}));

View File

@@ -15,8 +15,7 @@ tests = [
'server_helper',
'lrucache',
'i18n',
'response',
'spelling_correction'
'response'
]
if build_machine.system() != 'windows'
@@ -43,7 +42,6 @@ if gtest_dep.found() and not meson.is_cross_build()
'zimfile_raycharles_uncategorized.zim',
'corner_cases#&.zim',
'poor.zim',
'spelling_correction_test.zim',
'library.xml',
'lib_for_server_search_test.xml',
'customized_resources.txt',

View File

@@ -65,19 +65,19 @@ const ResourceCollection resources200Compressible{
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/index.css" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/index.css?cacheid=ae79e41a" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/index.js" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/index.js?cacheid=4e232c58" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/index.js?cacheid=cc456f1f" },
{ 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" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/isotope.pkgd.min.js?cacheid=2e48d392" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/kiwix.css" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/kiwix.css?cacheid=b4e29e64" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/kiwix.css?cacheid=3948b846" },
{ 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=42e90cb9" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/taskbar.css?cacheid=80d56607" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/viewer.js" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/viewer.js?cacheid=3208c3ed" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/viewer.js?cacheid=f8c5f4bf" },
{ 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" },
@@ -86,7 +86,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=08955948" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/languages.js?cacheid=a83f0e13" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/catalog/search" },
@@ -112,8 +112,6 @@ const ResourceCollection resources200Compressible{
const ResourceCollection resources200Uncompressible{
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/404.svg" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/404.svg?cacheid=b6d648af" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/500.svg" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/500.svg?cacheid=32eb0f20" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/bittorrent.png" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/bittorrent.png?cacheid=4f5c6882" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/blank.html" },
@@ -164,8 +162,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/print.css" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/print.css?cacheid=65b1c1d2" },
{ ZIM_CONTENT, "/ROOT%23%3F/raw/zimfile/meta/Title" },
{ ZIM_CONTENT, "/ROOT%23%3F/raw/zimfile/meta/Description" },
@@ -290,7 +286,7 @@ TEST_F(ServerTest, CacheIdsOfStaticResources)
const std::vector<UrlAndExpectedResult> testData{
{
/* url */ "/ROOT%23%3F/",
R"EXPECTEDRESULT( href="/ROOT%23%3F/skin/kiwix.css?cacheid=b4e29e64"
R"EXPECTEDRESULT( href="/ROOT%23%3F/skin/kiwix.css?cacheid=3948b846"
href="/ROOT%23%3F/skin/index.css?cacheid=ae79e41a"
<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">
@@ -301,10 +297,10 @@ R"EXPECTEDRESULT( href="/ROOT%23%3F/skin/kiwix.css?cacheid=b4e29e64"
<meta name="msapplication-config" content="/ROOT%23%3F/skin/favicon/browserconfig.xml?cacheid=f29a7c4a">
<script type="text/javascript" src="./skin/polyfills.js?cacheid=a0e0343d"></script>
<script type="module" src="/ROOT%23%3F/skin/i18n.js?cacheid=e9a10ac1" defer></script>
<script type="text/javascript" src="/ROOT%23%3F/skin/languages.js?cacheid=08955948" defer></script>
<script type="text/javascript" src="/ROOT%23%3F/skin/languages.js?cacheid=a83f0e13" 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=4e232c58" defer></script>
<script type="text/javascript" src="/ROOT%23%3F/skin/index.js?cacheid=cc456f1f" defer></script>
<img src="/ROOT%23%3F/skin/feed.svg?cacheid=055b333f"
<img src="/ROOT%23%3F/skin/langSelector.svg?cacheid=00b59961"
)EXPECTEDRESULT"
@@ -331,14 +327,13 @@ R"EXPECTEDRESULT( <img src="${root}/skin/download-white.svg?cac
},
{
/* url */ "/ROOT%23%3F/viewer",
R"EXPECTEDRESULT( <link type="text/css" href="./skin/kiwix.css?cacheid=b4e29e64" rel="Stylesheet" />
<link type="text/css" href="./skin/taskbar.css?cacheid=42e90cb9" rel="Stylesheet" />
R"EXPECTEDRESULT( <link type="text/css" href="./skin/kiwix.css?cacheid=3948b846" rel="Stylesheet" />
<link type="text/css" href="./skin/taskbar.css?cacheid=80d56607" rel="Stylesheet" />
<link type="text/css" href="./skin/autoComplete/css/autoComplete.css?cacheid=f2d376c4" rel="Stylesheet" />
<link type="text/css" href="./skin/print.css?cacheid=65b1c1d2" media="print" rel="Stylesheet" />
<script type="text/javascript" src="./skin/polyfills.js?cacheid=a0e0343d"></script>
<script type="module" src="./skin/i18n.js?cacheid=e9a10ac1" defer></script>
<script type="text/javascript" src="./skin/languages.js?cacheid=08955948" defer></script>
<script type="text/javascript" src="./skin/viewer.js?cacheid=3208c3ed" defer></script>
<script type="text/javascript" src="./skin/languages.js?cacheid=a83f0e13" defer></script>
<script type="text/javascript" src="./skin/viewer.js?cacheid=f8c5f4bf" defer></script>
<script type="text/javascript" src="./skin/autoComplete/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>
@@ -1011,6 +1006,7 @@ std::string expectedSexy404ErrorHtml(const std::string& url)
TEST_F(ServerTest, HttpSexy404HtmlError)
{
using namespace TestingOfHtmlResponses;
const std::vector<std::string> testUrls {
// XXX: Nicer 404 error page no longer hints whether the error
// XXX: is because of the missing book/ZIM-file or a missing article
@@ -1213,98 +1209,35 @@ TEST_F(ServerTest, HttpXmlError)
}
}
std::string expectedSexy500ErrorHtml(const std::string& url,
const std::string& error)
TEST_F(ServerTest, 500)
{
const auto urlWithoutQuery = replace(url, "\\?.*$", "");
const auto htmlSafeUrl = htmlEscape(urlWithoutQuery);
const auto jsSafeUrl = escapeJsString(urlWithoutQuery);
const std::string englishText[] = {
"Internal Server Error",
"Page isn&apos;t working",
"Oops. Page isn&apos;t working.",
"The requested path cannot be properly delivered:",
};
const std::string translatedText[] = {
"[I18N] Internal Server Error [TESTING]",
"Page [I18N] isn&apos;t [TESTING] working",
"Oops. [I18N] Page isn&apos;t [TESTING] working.",
"The requested path [I18N TESTING] cannot be properly delivered:",
};
const bool shouldTranslate = url.find("userlang=test") != std::string::npos;
const std::string* const t = shouldTranslate ? translatedText : englishText;
return R"RAWSTRINGLITERAL(<!DOCTYPE html>
<html>
const std::string expectedBody = R"(<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>)RAWSTRINGLITERAL" + t[0] + R"RAWSTRINGLITERAL(</title>
<link type="text/css" href="/ROOT%23%3F/skin/error.css?cacheid=b3fa90cf" rel="Stylesheet" />
<meta content="text/html;charset=UTF-8" http-equiv="content-type" />
<title>Internal Server Error</title>
<script>
window.KIWIX_RESPONSE_TEMPLATE = "&lt;!DOCTYPE html&gt;\n&lt;html&gt;\n &lt;head&gt;\n &lt;meta charset=&quot;utf-8&quot;&gt;\n &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no&quot; /&gt;\n &lt;title&gt;{{PAGE_TITLE}}&lt;/title&gt;\n &lt;link type=&quot;text/css&quot; href=&quot;{{root}}/skin/error.css?cacheid=b3fa90cf&quot; rel=&quot;Stylesheet&quot; /&gt;\n &lt;script&gt;\n window.KIWIX_RESPONSE_TEMPLATE = &quot;{{KIWIX_RESPONSE_TEMPLATE}}&quot;;\n window.KIWIX_RESPONSE_DATA = {{{KIWIX_RESPONSE_DATA}}};\n &lt;/script&gt;\n &lt;/head&gt;\n &lt;body&gt;\n &lt;header&gt;\n &lt;img src=&quot;{{root}}/skin/500.svg?cacheid=32eb0f20&quot;\n alt=&quot;{{500_img_text}}&quot;\n aria-label=&quot;{{500_img_text}}&quot;\n title=&quot;{{500_img_text}}&quot;&gt;\n &lt;/header&gt;\n &lt;section class=&quot;intro&quot;&gt;\n &lt;h1&gt;{{PAGE_HEADING}}&lt;/h1&gt;\n &lt;p&gt;{{PAGE_TEXT}}&lt;/p&gt;\n &lt;p&gt;&lt;code&gt;{{url_path}}&lt;/code&gt;&lt;/p&gt;\n &lt;/section&gt;\n{{#error}}\n &lt;section class=&quot;advice&quot;&gt;\n &lt;p&gt;{{error}}&lt;/p&gt;\n &lt;/section&gt;\n{{/error}}\n &lt;/body&gt;\n&lt;/html&gt;\n";
window.KIWIX_RESPONSE_DATA = { "500_img_text" : { "msgid" : "500-img-text", "params" : { } }, "PAGE_HEADING" : { "msgid" : "500-page-heading", "params" : { } }, "PAGE_TEXT" : { "msgid" : "500-page-text", "params" : { } }, "PAGE_TITLE" : { "msgid" : "500-page-title", "params" : { } }, "error" : ")RAWSTRINGLITERAL"
+ // inject the error
escapeJsString(error) // inject the error
+ // inject the error
R"RAWSTRINGLITERAL(", "root" : "/ROOT%23%3F", "url_path" : ")RAWSTRINGLITERAL"
+ // inject the URL
jsSafeUrl // inject the URL
+ // inject the URL
R"RAWSTRINGLITERAL(" };
window.KIWIX_RESPONSE_TEMPLATE = )" + ERROR_HTML_TEMPLATE_JS_STRING + R"(;
window.KIWIX_RESPONSE_DATA = { "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "500-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "500-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "500-page-text", "params" : { } } }, { "p" : { "msgid" : "non-translated-text", "params" : { "MSG" : "Entry redirect_loop.html is a redirect entry." } } } ] };
</script>
</head>
<body>
<header>
<img src="/ROOT%23%3F/skin/500.svg?cacheid=32eb0f20"
alt=")RAWSTRINGLITERAL" + t[1] + R"RAWSTRINGLITERAL("
aria-label=")RAWSTRINGLITERAL" + t[1] + R"RAWSTRINGLITERAL("
title=")RAWSTRINGLITERAL" + t[1] + R"RAWSTRINGLITERAL(">
</header>
<section class="intro">
<h1>)RAWSTRINGLITERAL" + t[2] + R"RAWSTRINGLITERAL(</h1>
<p>)RAWSTRINGLITERAL" + t[3] + R"RAWSTRINGLITERAL(</p>
<p><code>)RAWSTRINGLITERAL"
+ // inject the URL
htmlSafeUrl // inject the URL
+ // inject the URL
R"RAWSTRINGLITERAL(</code></p>
</section>
<section class="advice">
<p>)RAWSTRINGLITERAL" + error + R"RAWSTRINGLITERAL(</p>
</section>
<h1>Internal Server Error</h1>
<p>
An internal server error occured. We are sorry about that :/
</p>
<p>
Entry redirect_loop.html is a redirect entry.
</p>
</body>
</html>
)RAWSTRINGLITERAL";
}
)";
TEST_F(ServerTest, 500)
{
struct TestData {
const std::string url;
const std::string error;
};
const TestData testData[] = {
{
"/ROOT%23%3F/content/poor/A/redirect_loop.html",
"Entry redirect_loop.html is a redirect entry."
},
{
"/ROOT%23%3F/content/poor/A/redirect_loop.html?userlang=test",
"Entry redirect_loop.html is a redirect entry."
}
};
for ( const auto& td : testData ) {
const TestContext ctx{ {"url", td.url} };
const auto r = zfs1_->GET(td.url.c_str());
EXPECT_EQ(r->status, 500) << ctx;
EXPECT_EQ(r->get_header_value("Content-Type"), "text/html; charset=utf-8") << ctx;
EXPECT_EQ(r->body, expectedSexy500ErrorHtml(td.url, td.error)) << ctx;
{
const auto r = zfs1_->GET("/ROOT%23%3F/content/poor/A/redirect_loop.html");
EXPECT_EQ(r->status, 500);
EXPECT_EQ(r->body, expectedBody);
EXPECT_EQ(r->get_header_value("Content-Type"), "text/html; charset=utf-8");
}
}
@@ -1316,12 +1249,12 @@ R"EXPECTEDRESPONSE(const uiLanguages = [
{
"iso_code": "ar",
"self_name": "الإنجليزية",
"translation_count": 44
"translation_count": 43
},
{
"iso_code": "bn",
"self_name": "বাংলা",
"translation_count": 34
"translation_count": 24
},
{
"iso_code": "br",
@@ -1351,7 +1284,7 @@ R"EXPECTEDRESPONSE(const uiLanguages = [
{
"iso_code": "en",
"self_name": "English",
"translation_count": 93
"translation_count": 77
},
{
"iso_code": "es",
@@ -1366,7 +1299,7 @@ R"EXPECTEDRESPONSE(const uiLanguages = [
{
"iso_code": "fr",
"self_name": "Français",
"translation_count": 84
"translation_count": 68
},
{
"iso_code": "ha",
@@ -1376,27 +1309,22 @@ R"EXPECTEDRESPONSE(const uiLanguages = [
{
"iso_code": "he",
"self_name": "עברית",
"translation_count": 80
"translation_count": 69
},
{
"iso_code": "hi",
"self_name": "हिन्दी",
"translation_count": 59
},
{
"iso_code": "hu",
"self_name": "Magyar",
"translation_count": 32
},
{
"iso_code": "hy",
"self_name": "Հայերեն",
"translation_count": 26
"translation_count": 25
},
{
"iso_code": "ia",
"self_name": "interlingua",
"translation_count": 84
"translation_count": 67
},
{
"iso_code": "id",
@@ -1411,7 +1339,7 @@ R"EXPECTEDRESPONSE(const uiLanguages = [
{
"iso_code": "it",
"self_name": "italiano",
"translation_count": 58
"translation_count": 54
},
{
"iso_code": "ja",
@@ -1421,7 +1349,7 @@ R"EXPECTEDRESPONSE(const uiLanguages = [
{
"iso_code": "ko",
"self_name": "한국어",
"translation_count": 73
"translation_count": 64
},
{
"iso_code": "ku-latn",
@@ -1431,12 +1359,12 @@ R"EXPECTEDRESPONSE(const uiLanguages = [
{
"iso_code": "lb",
"self_name": "Lëtzebuergesch",
"translation_count": 48
"translation_count": 43
},
{
"iso_code": "mk",
"self_name": "македонски",
"translation_count": 81
"translation_count": 76
},
{
"iso_code": "ms",
@@ -1446,7 +1374,7 @@ R"EXPECTEDRESPONSE(const uiLanguages = [
{
"iso_code": "nb",
"self_name": "Engelsk",
"translation_count": 68
"translation_count": 50
},
{
"iso_code": "nl",
@@ -1471,7 +1399,7 @@ R"EXPECTEDRESPONSE(const uiLanguages = [
{
"iso_code": "pt-br",
"self_name": "Português",
"translation_count": 66
"translation_count": 65
},
{
"iso_code": "pt",
@@ -1481,7 +1409,7 @@ R"EXPECTEDRESPONSE(const uiLanguages = [
{
"iso_code": "ro",
"self_name": "Engleză",
"translation_count": 68
"translation_count": 67
},
{
"iso_code": "ru",
@@ -1501,7 +1429,7 @@ R"EXPECTEDRESPONSE(const uiLanguages = [
{
"iso_code": "skr-arab",
"self_name": "سرائیکی",
"translation_count": 33
"translation_count": 31
},
{
"iso_code": "sl",
@@ -1541,7 +1469,7 @@ R"EXPECTEDRESPONSE(const uiLanguages = [
{
"iso_code": "zh-hant",
"self_name": "繁體中文",
"translation_count": 92
"translation_count": 76
}
])EXPECTEDRESPONSE");
}

View File

@@ -758,8 +758,8 @@ struct TestData
? R"(No results were found for <b>"PATTERN"</b>)"
: R"(Results <b>FIRSTRESULT-LASTRESULT</b> of <b>RESULTCOUNT</b> for <b>"PATTERN"</b>)";
const size_t lastResultIndex = std::min(totalResultCount, firstResultIndex + results.size());
header = replace(header, "FIRSTRESULT", std::to_string(firstResultIndex+1));
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());
@@ -940,7 +940,7 @@ TEST(ServerSearchTest, searchResults)
/* start */ -1,
/* resultsPerPage */ 0,
/* totalResultCount */ 0,
/* firstResultIndex */ 0,
/* firstResultIndex */ 1,
/* results */ {},
/* pagination */ {}
},
@@ -951,7 +951,7 @@ TEST(ServerSearchTest, searchResults)
/* start */ -1,
/* resultsPerPage */ 0,
/* totalResultCount */ 0,
/* firstResultIndex */ 0,
/* firstResultIndex */ 1,
/* results */ {},
/* pagination */ {}
},
@@ -961,7 +961,7 @@ TEST(ServerSearchTest, searchResults)
/* start */ -1,
/* resultsPerPage */ 0,
/* totalResultCount */ 1,
/* firstResultIndex */ 0,
/* firstResultIndex */ 1,
/* results */ {
SEARCH_RESULT(
/*link*/ "/ROOT%23%3F/content/zimfile/A/We_Gonna_Move_to_the_Outskirts_of_Town",
@@ -979,7 +979,7 @@ TEST(ServerSearchTest, searchResults)
/* start */ -1,
/* resultsPerPage */ 0,
/* totalResultCount */ 2,
/* firstResultIndex */ 0,
/* firstResultIndex */ 1,
/* results */ {
SEARCH_RESULT(
/*link*/ "/ROOT%23%3F/content/zimfile/A/Eleanor_Rigby",
@@ -1005,7 +1005,7 @@ TEST(ServerSearchTest, searchResults)
/* start */ 0,
/* resultsPerPage */ 0,
/* totalResultCount */ 2,
/* firstResultIndex */ 0,
/* firstResultIndex */ 1,
/* results */ {
SEARCH_RESULT(
/*link*/ "/ROOT%23%3F/content/zimfile/A/Eleanor_Rigby",
@@ -1028,10 +1028,10 @@ TEST(ServerSearchTest, searchResults)
{
/* query */ "pattern=yellow&books.id=" RAYCHARLESZIMID,
/* start */ 0,
/* start */ 1,
/* resultsPerPage */ 0,
/* totalResultCount */ 2,
/* firstResultIndex */ 0,
/* firstResultIndex */ 1,
/* results */ {
SEARCH_RESULT(
/*link*/ "/ROOT%23%3F/content/zimfile/A/Eleanor_Rigby",
@@ -1057,7 +1057,7 @@ TEST(ServerSearchTest, searchResults)
/* start */ -1,
/* resultsPerPage */ 100,
/* totalResultCount */ 44,
/* firstResultIndex */ 0,
/* firstResultIndex */ 1,
/* results */ LARGE_SEARCH_RESULTS,
/* pagination */ {}
},
@@ -1068,7 +1068,7 @@ TEST(ServerSearchTest, searchResults)
/* start */ -1,
/* resultsPerPage */ 100,
/* totalResultCount */ 44,
/* firstResultIndex */ 0,
/* firstResultIndex */ 1,
/* results */ LARGE_SEARCH_RESULTS,
/* pagination */ {}
},
@@ -1078,7 +1078,7 @@ TEST(ServerSearchTest, searchResults)
/* start */ -1,
/* resultsPerPage */ 5,
/* totalResultCount */ 44,
/* firstResultIndex */ 0,
/* firstResultIndex */ 1,
/* results */ {
LARGE_SEARCH_RESULTS[0],
LARGE_SEARCH_RESULTS[1],
@@ -1099,10 +1099,10 @@ TEST(ServerSearchTest, searchResults)
{
/* query */ "pattern=jazz&books.id=" RAYCHARLESZIMID,
/* start */ 5,
/* start */ 6,
/* resultsPerPage */ 5,
/* totalResultCount */ 44,
/* firstResultIndex */ 5,
/* firstResultIndex */ 6,
/* results */ {
LARGE_SEARCH_RESULTS[5],
LARGE_SEARCH_RESULTS[6],
@@ -1124,10 +1124,10 @@ TEST(ServerSearchTest, searchResults)
{
/* query */ "pattern=jazz&books.id=" RAYCHARLESZIMID,
/* start */ 10,
/* start */ 11,
/* resultsPerPage */ 5,
/* totalResultCount */ 44,
/* firstResultIndex */ 10,
/* firstResultIndex */ 11,
/* results */ {
LARGE_SEARCH_RESULTS[10],
LARGE_SEARCH_RESULTS[11],
@@ -1150,10 +1150,10 @@ TEST(ServerSearchTest, searchResults)
{
/* query */ "pattern=jazz&books.id=" RAYCHARLESZIMID,
/* start */ 15,
/* start */ 16,
/* resultsPerPage */ 5,
/* totalResultCount */ 44,
/* firstResultIndex */ 15,
/* firstResultIndex */ 16,
/* results */ {
LARGE_SEARCH_RESULTS[15],
LARGE_SEARCH_RESULTS[16],
@@ -1177,10 +1177,10 @@ TEST(ServerSearchTest, searchResults)
{
/* query */ "pattern=jazz&books.id=" RAYCHARLESZIMID,
/* start */ 20,
/* start */ 21,
/* resultsPerPage */ 5,
/* totalResultCount */ 44,
/* firstResultIndex */ 20,
/* firstResultIndex */ 21,
/* results */ {
LARGE_SEARCH_RESULTS[20],
LARGE_SEARCH_RESULTS[21],
@@ -1204,10 +1204,10 @@ TEST(ServerSearchTest, searchResults)
{
/* query */ "pattern=jazz&books.id=" RAYCHARLESZIMID,
/* start */ 25,
/* start */ 26,
/* resultsPerPage */ 5,
/* totalResultCount */ 44,
/* firstResultIndex */ 25,
/* firstResultIndex */ 26,
/* results */ {
LARGE_SEARCH_RESULTS[25],
LARGE_SEARCH_RESULTS[26],
@@ -1231,10 +1231,10 @@ TEST(ServerSearchTest, searchResults)
{
/* query */ "pattern=jazz&books.id=" RAYCHARLESZIMID,
/* start */ 30,
/* start */ 31,
/* resultsPerPage */ 5,
/* totalResultCount */ 44,
/* firstResultIndex */ 30,
/* firstResultIndex */ 31,
/* results */ {
LARGE_SEARCH_RESULTS[30],
LARGE_SEARCH_RESULTS[31],
@@ -1257,10 +1257,10 @@ TEST(ServerSearchTest, searchResults)
{
/* query */ "pattern=jazz&books.id=" RAYCHARLESZIMID,
/* start */ 35,
/* start */ 36,
/* resultsPerPage */ 5,
/* totalResultCount */ 44,
/* firstResultIndex */ 35,
/* firstResultIndex */ 36,
/* results */ {
LARGE_SEARCH_RESULTS[35],
LARGE_SEARCH_RESULTS[36],
@@ -1282,10 +1282,10 @@ TEST(ServerSearchTest, searchResults)
{
/* query */ "pattern=jazz&books.id=" RAYCHARLESZIMID,
/* start */ 40,
/* start */ 41,
/* resultsPerPage */ 5,
/* totalResultCount */ 44,
/* firstResultIndex */ 40,
/* firstResultIndex */ 41,
/* results */ {
LARGE_SEARCH_RESULTS[40],
LARGE_SEARCH_RESULTS[41],
@@ -1305,10 +1305,10 @@ TEST(ServerSearchTest, searchResults)
{
/* query */ "pattern=jazz&books.id=" RAYCHARLESZIMID,
/* start */ 21,
/* start */ 22,
/* resultsPerPage */ 3,
/* totalResultCount */ 44,
/* firstResultIndex */ 21,
/* firstResultIndex */ 22,
/* results */ {
LARGE_SEARCH_RESULTS[21],
LARGE_SEARCH_RESULTS[22],
@@ -1334,10 +1334,10 @@ TEST(ServerSearchTest, searchResults)
// works, not how it should work!
{
/* query */ "pattern=jazz&books.id=" RAYCHARLESZIMID,
/* start */ 45,
/* start */ 46,
/* resultsPerPage */ 5,
/* totalResultCount */ 44,
/* firstResultIndex */ 45,
/* firstResultIndex */ 46,
/* results */ {},
/* pagination */ {
@@ -1357,7 +1357,7 @@ TEST(ServerSearchTest, searchResults)
/* start */ 0,
/* resultsPerPage */ 10,
/* totalResultCount */ 2,
/* firstResultIndex */ 0,
/* firstResultIndex */ 1,
/* results */ {
SEARCH_RESULT_FOR_TRAVEL_IN_RAYCHARLESZIM,
SEARCH_RESULT_FOR_TRAVEL_IN_EXAMPLEZIM
@@ -1371,7 +1371,7 @@ TEST(ServerSearchTest, searchResults)
/* start */ 0,
/* resultsPerPage */ 10,
/* totalResultCount */ 2,
/* firstResultIndex */ 0,
/* firstResultIndex */ 1,
/* results */ {
SEARCH_RESULT_FOR_TRAVEL_IN_RAYCHARLESZIM,
SEARCH_RESULT_FOR_TRAVEL_IN_EXAMPLEZIM
@@ -1386,7 +1386,7 @@ TEST(ServerSearchTest, searchResults)
/* start */ 0,
/* resultsPerPage */ 10,
/* totalResultCount */ 1,
/* firstResultIndex */ 0,
/* firstResultIndex */ 1,
/* results */ {
SEARCH_RESULT_FOR_TRAVEL_IN_RAYCHARLESZIM
},
@@ -1401,7 +1401,7 @@ TEST(ServerSearchTest, searchResults)
/* start */ 0,
/* resultsPerPage */ 10,
/* totalResultCount */ 1,
/* firstResultIndex */ 0,
/* firstResultIndex */ 1,
/* results */ {
SEARCH_RESULT_FOR_TRAVEL_IN_EXAMPLEZIM
},
@@ -1416,7 +1416,7 @@ TEST(ServerSearchTest, searchResults)
/* start */ 0,
/* resultsPerPage */ 10,
/* totalResultCount */ 1,
/* firstResultIndex */ 0,
/* firstResultIndex */ 1,
/* results */ {
SEARCH_RESULT_FOR_TRAVEL_IN_RAYCHARLESZIM
},
@@ -1431,7 +1431,7 @@ TEST(ServerSearchTest, searchResults)
/* start */ 0,
/* resultsPerPage */ 10,
/* totalResultCount */ 1,
/* firstResultIndex */ 0,
/* firstResultIndex */ 1,
/* results */ {
SEARCH_RESULT_FOR_TRAVEL_IN_EXAMPLEZIM
},
@@ -1447,7 +1447,7 @@ TEST(ServerSearchTest, searchResults)
/* start */ -1,
/* resultsPerPage */ 100,
/* totalResultCount */ 44,
/* firstResultIndex */ 0,
/* firstResultIndex */ 1,
/* results */ LARGE_SEARCH_RESULTS,
/* pagination */ {}
},
@@ -1458,7 +1458,7 @@ TEST(ServerSearchTest, searchResults)
/* start */ -1,
/* resultsPerPage */ 5,
/* totalResultCount */ 44,
/* firstResultIndex */ 0,
/* firstResultIndex */ 1,
/* results */ {
LARGE_SEARCH_RESULTS[0],
LARGE_SEARCH_RESULTS[1],
@@ -1483,7 +1483,7 @@ TEST(ServerSearchTest, searchResults)
/* start */ -1,
/* resultsPerPage */ 5,
/* totalResultCount */ 44,
/* firstResultIndex */ 0,
/* firstResultIndex */ 1,
/* results */ {
LARGE_SEARCH_RESULTS[0],
LARGE_SEARCH_RESULTS[1],
@@ -1509,7 +1509,7 @@ TEST(ServerSearchTest, searchResults)
/* start */ -1,
/* resultsPerPage */ 5,
/* totalResultCount */ 44,
/* firstResultIndex */ 0,
/* firstResultIndex */ 1,
/* results */ {
LARGE_SEARCH_RESULTS[0],
LARGE_SEARCH_RESULTS[1],
@@ -1536,7 +1536,7 @@ TEST(ServerSearchTest, searchResults)
/* start */ -1,
/* resultsPerPage */ 5,
/* totalResultCount */ 44,
/* firstResultIndex */ 0,
/* firstResultIndex */ 1,
/* results */ {
LARGE_SEARCH_RESULTS[0],
LARGE_SEARCH_RESULTS[1],

View File

@@ -62,7 +62,6 @@ public: // types
WITH_LIBRARY_BUTTON = 1 << 2,
BLOCK_EXTERNAL_LINKS = 1 << 3,
NO_NAME_MAPPER = 1 << 4,
CATALOG_ONLY_MODE = 1 << 5,
WITH_TASKBAR_AND_LIBRARY_BUTTON = WITH_TASKBAR | WITH_LIBRARY_BUTTON,
@@ -72,7 +71,6 @@ public: // types
struct Cfg
{
std::string root = "ROOT#?";
std::string contentServerUrl = "";
Options options = DEFAULT_OPTIONS;
Cfg(Options opts = DEFAULT_OPTIONS) : options(opts) {}
@@ -151,8 +149,6 @@ void ZimFileServer::run(int serverPort, std::string indexTemplateString)
server->setTaskbar(cfg.options & WITH_TASKBAR, cfg.options & WITH_LIBRARY_BUTTON);
server->setBlockExternalLinks(cfg.options & BLOCK_EXTERNAL_LINKS);
server->setMultiZimSearchLimit(3);
server->setCatalogOnlyMode(cfg.options & CATALOG_ONLY_MODE);
server->setContentServerUrl(cfg.contentServerUrl);
if (!indexTemplateString.empty()) {
server->setIndexTemplateString(indexTemplateString);
}

View File

@@ -1,217 +0,0 @@
/*
* Copyright (C) 2025 Veloman Yunkan
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* is provided AS IS, WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, and
* NON-INFRINGEMENT. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*
*/
#include "gtest/gtest.h"
#include "../include/spelling_correction.h"
#include "../src/tools/pathTools.h"
#include "zim/archive.h"
#include <filesystem>
#include <xapian.h>
const std::string TEST_DB_PATH = "./spellings.db";
class SpellingCorrectionTest : public ::testing::Test
{
protected:
void SetUp() override {
tmpDirPath = makeTmpDirectory();
archive = std::make_unique<zim::Archive>("./test/spelling_correction_test.zim");
}
void TearDown() override {
std::filesystem::remove_all(tmpDirPath);
}
protected:
std::filesystem::path tmpDirPath;
std::unique_ptr<zim::Archive> archive;
};
void testSpellingCorrections(const kiwix::SpellingsDB& spellingsDB)
{
#define EXPECT_SPELLING_CORRECTION(query, maxSuggestions, parenthesizedExpectedResult) \
EXPECT_EQ( \
spellingsDB.getSpellingCorrections(query, maxSuggestions), \
std::vector<std::string> parenthesizedExpectedResult \
)
EXPECT_SPELLING_CORRECTION("", 1, ({}));
EXPECT_SPELLING_CORRECTION("geflekt", 1, ({"gefleckt"}));
EXPECT_SPELLING_CORRECTION("Teler", 1, ({"Teller"}));
EXPECT_SPELLING_CORRECTION("Teler", 1, ({"Teller"}));
EXPECT_SPELLING_CORRECTION("kämen", 1, ({"kämmen"}));
EXPECT_SPELLING_CORRECTION("abonieren", 1, ({"abonnieren"}));
EXPECT_SPELLING_CORRECTION("abbonnieren", 1, ({"abonnieren"}));
EXPECT_SPELLING_CORRECTION("abbonieren", 1, ({"abonnieren"}));
EXPECT_SPELLING_CORRECTION("Aplaus", 1, ({"Applaus"}));
EXPECT_SPELLING_CORRECTION("konkurieren", 1, ({"konkurrieren"}));
EXPECT_SPELLING_CORRECTION("Asisstent", 1, ({"Assistent"}));
EXPECT_SPELLING_CORRECTION("Assisstent", 1, ({"Assistent"}));
EXPECT_SPELLING_CORRECTION("Atacke", 1, ({"Attacke"}));
EXPECT_SPELLING_CORRECTION("atestieren", 1, ({"attestieren"}));
EXPECT_SPELLING_CORRECTION("entäuschen", 1, ({"enttäuschen"}));
EXPECT_SPELLING_CORRECTION("Enzündung", 1, ({"Entzündung"}));
EXPECT_SPELLING_CORRECTION("Schirmütze", 1, ({"Schirmmütze"}));
EXPECT_SPELLING_CORRECTION("Termoskanne", 1, ({"Thermoskanne"}));
EXPECT_SPELLING_CORRECTION("Tsunge", 1, ({"Zunge"}));
EXPECT_SPELLING_CORRECTION("vort", 1, ({"fort"}));
EXPECT_SPELLING_CORRECTION("Schtuhl", 1, ({"Stuhl"}));
EXPECT_SPELLING_CORRECTION("beissen", 1, ({"beißen"}));
EXPECT_SPELLING_CORRECTION("Camera", 1, ({"Kamera"}));
EXPECT_SPELLING_CORRECTION("Kaos", 1, ({"Chaos"}));
// The spelling correction "Lax -> Lachs" is affected by commit
// https://github.com/xapian/xapian/commit/0cbe35de5c392623388946e6769aa03f912fdde4
// which caps the edit distance at (length(query_word) - 1). As a result, the
// max edit distance parameter that we pass into get_spelling_suggestion() is
// reduced from 3 to 2 and is below the edit distance of "Lachs" from "Lax".
const auto xapianVersion = std::make_tuple(Xapian::major_version(),
Xapian::minor_version(),
Xapian::revision());
if ( xapianVersion < std::make_tuple(1, 4, 19) ) {
EXPECT_SPELLING_CORRECTION("Lax", 1, ({"Lachs"}));
} else {
EXPECT_SPELLING_CORRECTION("Lax", 1, ({}));
}
EXPECT_SPELLING_CORRECTION("Mont", 1, ({"Mond"}));
EXPECT_SPELLING_CORRECTION("Umweltstandart", 1, ({"Umweltstandard"}));
EXPECT_SPELLING_CORRECTION("seid", 1, ({"seit"}));
EXPECT_SPELLING_CORRECTION("Trok", 1, ({"Trog"}));
EXPECT_SPELLING_CORRECTION("Unfuk", 1, ({"Unfug"}));
EXPECT_SPELLING_CORRECTION("schupsen", 1, ({"schubsen"}));
EXPECT_SPELLING_CORRECTION("warscheinlich", 1, ({"wahrscheinlich"}));
EXPECT_SPELLING_CORRECTION("gefärlich", 1, ({"gefährlich"}));
EXPECT_SPELLING_CORRECTION("Son", 1, ({"Sohn"}));
EXPECT_SPELLING_CORRECTION("nähmlich", 1, ({"nämlich"}));
EXPECT_SPELLING_CORRECTION("Grahl", 1, ({"Gral"}));
EXPECT_SPELLING_CORRECTION("Bine", 1, ({"Biene"}));
EXPECT_SPELLING_CORRECTION("Hirarchie", 1, ({"Hierarchie"}));
EXPECT_SPELLING_CORRECTION("Priese", 1, ({"Prise"}));
EXPECT_SPELLING_CORRECTION("auslehren", 1, ({"ausleeren"}));
EXPECT_SPELLING_CORRECTION("Phenomen", 1, ({"Phänomen"}));
EXPECT_SPELLING_CORRECTION("Phänomän", 1, ({"Phänomen"}));
EXPECT_SPELLING_CORRECTION("Phenomän", 1, ({"Phänomen"}));
EXPECT_SPELLING_CORRECTION("gewehren", 1, ({"gewähren"}));
EXPECT_SPELLING_CORRECTION("aba", 1, ({"aber"}));
EXPECT_SPELLING_CORRECTION("gestan", 1, ({"gestern"}));
EXPECT_SPELLING_CORRECTION("ronterfallen", 1, ({"runterfallen"}));
EXPECT_SPELLING_CORRECTION("Hönig", 1, ({"Honig"}));
EXPECT_SPELLING_CORRECTION("mussen", 1, ({"müssen"}));
EXPECT_SPELLING_CORRECTION("Bewandnis", 1, ({"Bewandtnis"}));
EXPECT_SPELLING_CORRECTION("hässlig", 1, ({"hässlich"}));
EXPECT_SPELLING_CORRECTION("lustich", 1, ({"lustig"}));
EXPECT_SPELLING_CORRECTION("Botschaftler", 1, ({"Botschafter"}));
EXPECT_SPELLING_CORRECTION("ebemfalls", 1, ({"ebenfalls"}));
EXPECT_SPELLING_CORRECTION("samft", 1, ({"sanft"}));
EXPECT_SPELLING_CORRECTION("Wohenzimmer", 1, ({"Wohnzimmer"}));
EXPECT_SPELLING_CORRECTION("Flaster", 1, ({"Pflaster"}));
EXPECT_SPELLING_CORRECTION("Imfung", 1, ({"Impfung"}));
EXPECT_SPELLING_CORRECTION("amptieren", 1, ({"amtieren"}));
EXPECT_SPELLING_CORRECTION("Endgeld", 1, ({"Entgelt"}));
EXPECT_SPELLING_CORRECTION("Abendteuer", 1, ({"Abenteuer"}));
EXPECT_SPELLING_CORRECTION("sampft", 1, ({"sanft"}));
EXPECT_SPELLING_CORRECTION("forgestan", 1, ({"vorgestern"}));
EXPECT_SPELLING_CORRECTION("Füreschein", 1, ({"Führerschein"}));
EXPECT_SPELLING_CORRECTION("ronterfalen", 1, ({"runterfallen"}));
EXPECT_SPELLING_CORRECTION("Farradschluss", 1, ({"Fahrradschloss"}));
EXPECT_SPELLING_CORRECTION("Konkorenz", 1, ({"Konkurrenz"}));
EXPECT_SPELLING_CORRECTION("Hirachie", 1, ({"Hierarchie"}));
//////////////////////////////////////////////////////////////////////////////
// Edge cases
//////////////////////////////////////////////////////////////////////////////
// Exact match is not considered a spelling correction
EXPECT_SPELLING_CORRECTION("Führerschein", 1, ({}));
// Max edit distance is 3
EXPECT_SPELLING_CORRECTION( "Führersch", 1, ({"Führerschein"}));
EXPECT_SPELLING_CORRECTION("Führersc", 1, ({}));
// Case matters in edit distance
EXPECT_SPELLING_CORRECTION("führersch", 1, ({}));
// Diacritics matters in edit distance
EXPECT_SPELLING_CORRECTION("Fuhrersch", 1, ({}));
// Mismatch in diacritics counts as 1 in edit distance (this is not trivial,
// because from the UTF-8 perspective it is a one-byte vs two-byte encoding
// of a Unicode codepoint).
EXPECT_SPELLING_CORRECTION("Führersche", 1, ({"Führerschein"}));
EXPECT_SPELLING_CORRECTION("Führershine", 1, ({"Führerschein"}));
EXPECT_SPELLING_CORRECTION("Führershyne", 1, ({}));
EXPECT_SPELLING_CORRECTION("führershine", 1, ({}));
EXPECT_SPELLING_CORRECTION("Führerschrom", 1, ({"Führerschein"}));
EXPECT_SPELLING_CORRECTION("Führerscdrom", 1, ({}));
//////////////////////////////////////////////////////////////////////////////
// Shortcomings of the proof-of-concept implementation
//////////////////////////////////////////////////////////////////////////////
// Multiword titles are treated as a single entity
EXPECT_SPELLING_CORRECTION("Laurem", 1, ({}));
EXPECT_SPELLING_CORRECTION("ibsum", 1, ({}));
EXPECT_SPELLING_CORRECTION("Loremipsum", 1, ({"Lorem ipsum"}));
// Only one spelling correction can be requested
// EXPECT_SPELLING_CORRECTION("Kung", 2, ({"King", "Kong"}));
EXPECT_THROW(spellingsDB.getSpellingCorrections("Kung", 2), std::runtime_error);
}
using StrCollection = std::vector<std::string>;
StrCollection directoryEntries(std::filesystem::path dirPath)
{
StrCollection result;
for ( const auto& dirEntry : std::filesystem::directory_iterator(dirPath) ) {
result.push_back(dirEntry.path().string());
}
return result;
}
TEST_F(SpellingCorrectionTest, allInOne)
{
const auto tmpDirModTime0 = std::filesystem::last_write_time(tmpDirPath);
ASSERT_TRUE(directoryEntries(tmpDirPath).empty());
{
const kiwix::SpellingsDB spellingsDB(*archive, tmpDirPath);
testSpellingCorrections(spellingsDB);
}
const auto tmpDirModTime1 = std::filesystem::last_write_time(tmpDirPath);
const auto spellingsDbPath = tmpDirPath / "554c9707-897e-097a-53ba-1b1306d8bb88.spellingsdb.v0.1";
const StrCollection EXPECTED_DIR_CONTENT{ spellingsDbPath.string() };
ASSERT_EQ(directoryEntries(tmpDirPath), EXPECTED_DIR_CONTENT);
ASSERT_LT(tmpDirModTime0, tmpDirModTime1);
const auto fileModTime = std::filesystem::last_write_time(spellingsDbPath);
{
const kiwix::SpellingsDB spellingsDB(*archive, tmpDirPath);
testSpellingCorrections(spellingsDB);
}
ASSERT_EQ(directoryEntries(tmpDirPath), EXPECTED_DIR_CONTENT );
ASSERT_EQ(tmpDirModTime1, std::filesystem::last_write_time(tmpDirPath));
ASSERT_EQ(fileModTime, std::filesystem::last_write_time(spellingsDbPath));
}