Compare commits

..

1 Commits

Author SHA1 Message Date
Veloman Yunkan
f8fc60ed2f Enabled smart mode of suggestions 2025-05-30 12:33:05 +04:00
77 changed files with 382 additions and 1721 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

@@ -1,31 +1,3 @@
libkiwix 14.1.1
===============
* Server:
- Fix regression for kiwix-serve --nosearchbar (@veloman-yunkan #1250)
- Avoid results content interpretation... crash in fulltext search (@vighnesh-sawant #1241)
- Fix for intermittent /content/blank.html errors (@veloman-yunkan #1249)
libkiwix 14.1.0
===============
* Server:
- Viewer detects & tracks intrapage navigation anchors too (@veloman-yunkan #1213)
- Add support for catalog only mode (@veloman-yunkan #1219)
- Add API which returns server access url (@vighnesh-sawant #1234)
- Fix chrome searchbar placeholder text overflow (@aditii2712 #1185)
- Fix magnet link queryStyring (@rgaudin #1160)
- Improve chrome printing stylesheet (@kelson42 #1202)
- Default white background (@kelson42 #1205)
* Other:
- Switched to the new libzim illustrations API (@veloman-yunkan #1226)
- Stop building Windows with DEBUG symbols in CI (@kelson42 #1165)
- Update many things in the CI/CD (@kelson42 #1203 #1194 #1209 #1207 #1235)
- Requires now libzim 9.4.0 (@kelson42 #1231)
- Fix compilation for FreeBSD (@OICe2 #1173 #1174)
- Wait up to 1s to let aria2c to start before complaining (@kelson42 #1169)
libkiwix 14.0.0
===============

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

@@ -155,15 +155,6 @@ class Manager
const std::string& url = "",
const bool checkMetaData = false);
/**
* Add all books from the directory tree into the library.
*
* @param path The path of the directory to scan.
* @param verboseFlag Verbose logs flag.
*/
void addBooksFromDirectory(const std::string& path,
const bool verboseFlag = false);
std::string writableLibraryPath;
bool m_hasSearchResult = false;

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,13 +63,10 @@ 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;
IpMode getIpMode() const;
std::vector<std::string> getServerAccessUrls() const;
protected:
std::shared_ptr<Library> mp_library;
@@ -86,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

@@ -1,5 +1,5 @@
project('libkiwix', 'cpp',
version : '14.1.1',
version : '14.0.0',
license : 'GPLv3+',
default_options : ['c_std=c11', 'cpp_std=c++17', 'werror=true'])
@@ -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

@@ -23,14 +23,6 @@
#include "tools/pathTools.h"
#include <pugixml.hpp>
#include <filesystem>
#include <iostream>
#include <set>
#include <queue>
#include <cctype>
#include <algorithm>
namespace fs = std::filesystem;
namespace kiwix
{
@@ -259,58 +251,6 @@ bool Manager::addBookFromPath(const std::string& pathToOpen,
.empty());
}
void Manager::addBooksFromDirectory(const std::string& path,
const bool verboseFlag)
{
std::set<std::string> iteratedDirs;
std::queue<std::string> dirQueue;
dirQueue.push(fs::absolute(path).u8string());
int totalBooksAdded = 0;
if (verboseFlag)
std::cout << "Adding books from the directory tree: " << dirQueue.front() << std::endl;
while (!dirQueue.empty()) {
const auto currentPath = dirQueue.front();
dirQueue.pop();
if (verboseFlag)
std::cout << "Visiting directory: " << currentPath << std::endl;
for (const auto& dirEntry : fs::directory_iterator(currentPath)) {
auto resolvedPath = dirEntry.path();
if (fs::is_symlink(dirEntry)) {
try {
resolvedPath = fs::canonical(dirEntry.path());
} catch (const std::exception& e) {
std::cerr << "Could not resolve symlink " << resolvedPath.u8string() << " to a valid path. Skipping..." << std::endl;
continue;
}
}
const std::string pathString = resolvedPath.u8string();
std::string resolvedPathExtension = resolvedPath.extension();
std::transform(resolvedPathExtension.begin(), resolvedPathExtension.end(), resolvedPathExtension.begin(),
[](unsigned char c){ return std::tolower(c); });
if (fs::is_directory(resolvedPath)) {
if (iteratedDirs.find(pathString) == iteratedDirs.end())
dirQueue.push(pathString);
else if (verboseFlag)
std::cout << "Already iterated over " << pathString << ". Skipping..." << std::endl;
} else if (resolvedPathExtension == ".zim" || resolvedPathExtension == ".zimaa") {
if (!this->addBookFromPath(pathString, pathString, "", false)) {
std::cerr << "Could not add " << pathString << " into the library." << std::endl;
} else if (verboseFlag) {
std::cout << "Added " << pathString << " into the library." << std::endl;
totalBooksAdded++;
}
} else if (verboseFlag) {
std::cout << "Skipped " << pathString << " - unsupported file type or permission denied." << std::endl;
}
}
iteratedDirs.insert(currentPath);
}
if (verboseFlag)
std::cout << "Traversal completed. Total books added: " << totalBooksAdded << std::endl;
}
bool Manager::readBookFromPath(const std::string& path, kiwix::Book* book)
{
std::string tmp_path = path;

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

@@ -29,22 +29,6 @@
namespace kiwix {
namespace
{
std::string makeServerUrl(std::string host, int port, std::string root)
{
const int httpDefaultPort = 80;
if (port == httpDefaultPort) {
return "http://" + host + root;
} else {
return "http://" + host + ":" + std::to_string(port) + root;
}
}
} // unnamed namespace
Server::Server(LibraryPtr library, std::shared_ptr<NameMapper> nameMapper) :
mp_library(library),
mp_nameMapper(nameMapper),
@@ -69,16 +53,8 @@ bool Server::start() {
m_blockExternalLinks,
m_ipMode,
m_indexTemplateString,
m_ipConnectionLimit,
m_catalogOnlyMode,
m_contentServerUrl));
if (mp_server->start()) {
// this syncs m_addr of InternalServer and Server as they may diverge
m_addr = mp_server->getAddress();
return true;
} else {
return false;
}
m_ipConnectionLimit));
return mp_server->start();
}
void Server::stop() {
@@ -91,12 +67,12 @@ void Server::stop() {
void Server::setRoot(const std::string& root)
{
m_root = root;
while (!m_root.empty() && m_root.back() == '/')
m_root.pop_back();
while (!m_root.empty() && m_root.front() == '/')
m_root = m_root.substr(1);
m_root = m_root.empty() ? m_root : "/" + m_root;
if (m_root[0] != '/') {
m_root = "/" + m_root;
}
if (m_root.back() == '/') {
m_root.erase(m_root.size() - 1);
}
}
void Server::setAddress(const std::string& addr)
@@ -115,12 +91,12 @@ void Server::setAddress(const std::string& addr)
int Server::getPort() const
{
return m_port;
return mp_server->getPort();
}
IpAddress Server::getAddress() const
{
return m_addr;
return mp_server->getAddress();
}
IpMode Server::getIpMode() const
@@ -128,16 +104,4 @@ IpMode Server::getIpMode() const
return mp_server->getIpMode();
}
std::vector<std::string> Server::getServerAccessUrls() const
{
std::vector<std::string> result;
if (!m_addr.addr.empty()) {
result.push_back(makeServerUrl(m_addr.addr, m_port, m_root));
}
if (!m_addr.addr6.empty()) {
result.push_back(makeServerUrl("[" + m_addr.addr6 + "]", m_port, m_root));
}
return result;
}
}

View File

@@ -99,6 +99,16 @@ bool ipAvailable(const std::string addr)
return false;
}
inline std::string normalizeRootUrl(std::string rootUrl)
{
while ( !rootUrl.empty() && rootUrl.back() == '/' )
rootUrl.pop_back();
while ( !rootUrl.empty() && rootUrl.front() == '/' )
rootUrl = rootUrl.substr(1);
return rootUrl.empty() ? rootUrl : "/" + rootUrl;
}
std::string
fullURL2LocalURL(const std::string& fullUrl, const std::string& rootLocation)
{
@@ -115,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&) {}
@@ -425,12 +432,10 @@ 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(root),
m_root(normalizeRootUrl(root)),
m_rootPrefixOfDecodedURL(m_root),
m_nbThreads(nbThreads),
m_multizimSearchLimit(multizimSearchLimit),
@@ -446,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);
}
@@ -706,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");
}
}
@@ -770,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) {
@@ -788,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);
@@ -840,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();
@@ -856,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();
@@ -1011,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);
@@ -1110,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);
@@ -1141,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,8 +3,6 @@
"authors": [
"Asma",
"Hamoudak",
"Meno25",
"Mohammed Qays",
"Ravan",
"محمد أحمد عبد الفتاح"
]
@@ -25,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,24 +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,7 +5,6 @@
"IMayBeABitShy",
"Justman10000",
"Lucas Werkmeister",
"MoritzMT20",
"Rofiatmustapha12",
"ThisCarthing"
]
@@ -38,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

@@ -1,18 +1,13 @@
{
"@metadata": {
"authors": [
"Jimkats",
"Kelson",
"Norhorn",
"Ανώνυμος Βικιπαιδιστής"
]
},
"name": "Αγγλικά",
"suggest-full-text-search": "περιέχει '{{{SEARCH_TERMS}}}'...",
"no-such-book": "Δεν υπάρχει τέτοιο βιβλίο: {{BOOK_NAME}}",
"caution-warning": "Προσοχή!",
"search-result-book-info": "από {{BOOK_TITLE}}",
"word-count": "{{COUNT}} λέξεις",
"welcome-page-overzealous-filter": "Κανένα αποτέλεσμα. Θέλετε να <a href=\"{{URL}}\">επαναφέρετε το φίλτρο</a>;",
"powered-by-kiwix-html": "Με την υποστήριξη by&nbsp;<a href=\"https://kiwix.org\">Kiwix</a>",
"search": "Αναζήτηση",
@@ -24,10 +19,10 @@
"direct-download-alt-text": "άμεση λήψη",
"hash-download-alt-text": "λήψη αναγνωριστικού",
"magnet-alt-text": "λήψη μαγνήτη",
"torrent-download-link-text": "BitTorrent",
"torrent-download-alt-text": "Λήψη μέσω BitTorrent",
"filter-by-tag": ιλτράρισμα κατά ετικέτα \"{{{TAG}}}\"",
"stop-filtering-by-tag": "Διακοπή φιλτραρίσματος κατά ετικέτα \"{{{TAG}}}\"",
"torrent-download-link-text": "Αρχείο torrent",
"torrent-download-alt-text": "λήψη torrent",
"filter-by-tag": ίλτρο ανά ετικέτα \"{{TAG}}\"",
"stop-filtering-by-tag": "Διακοπή φίλτρου ανά ετικέτα \"{{TAG}}\"",
"welcome-to-kiwix-server": "Καλώς ορίσατε στον διακομιστή Kiwix",
"download-links-heading": "Λήψη συνδέσμων για <b><i>{{BOOK_TITLE}}</i></b>",
"download-links-title": "Κατεβάστε το βιβλίο",

View File

@@ -30,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"
@@ -47,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,18 +3,17 @@
"authors": [
"AlexanderFF",
"Fitoschido",
"Laranxa",
"Ovruni",
"Sinopsistrans",
"SpikeShroom",
"Vis4valentine"
]
},
"name": "Español",
"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 :(",
@@ -27,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}}",
@@ -54,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>",
@@ -82,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",
@@ -99,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,8 +3,7 @@
"authors": [
"MITO",
"Nike",
"Pyscowicz",
"Samoasambia"
"Pyscowicz"
]
},
"name": "suomi",
@@ -15,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",
@@ -38,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",
@@ -32,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}}",
@@ -59,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

@@ -22,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}}",
@@ -43,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>",
@@ -82,6 +72,5 @@
"book-category.wikiversity": "ויקיברסיטה",
"book-category.wikivoyage": "ויקימסע",
"book-category.wiktionary": "ויקימילון",
"book-category.other": "אחר",
"text-loading-content": "התוכן נטען"
"book-category.other": "אחר"
}

View File

@@ -1,39 +0,0 @@
{
"@metadata": {
"authors": [
"Eukarióta",
"Urbalazs"
]
},
"name": "Magyar",
"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

@@ -13,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

@@ -22,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}}",
@@ -49,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>",
@@ -86,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

@@ -5,8 +5,7 @@
"Beta16",
"Clorofolle",
"Luca.favorido",
"McDutchie",
"Wheelygay"
"McDutchie"
]
},
"name": "italiano",
@@ -27,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

@@ -1,7 +1,6 @@
{
"@metadata": {
"authors": [
"YeBoy371",
"Ykhwong"
]
},
@@ -22,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}}",
@@ -40,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

@@ -19,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}}",
@@ -30,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

@@ -22,25 +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.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": "На пат сте да го напуштите 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 +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\">Кивикс</a>",

View File

@@ -25,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}}",
@@ -52,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

@@ -27,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

@@ -36,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

@@ -23,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}}",
@@ -49,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>",
@@ -62,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

@@ -1,7 +1,6 @@
{
"@metadata": {
"authors": [
"Apq",
"Jopparn",
"Larsa",
"Rofiatmustapha12",
@@ -26,25 +25,9 @@
"400-page-heading": "Ogiltig begäran",
"404-page-title": "Innehållet hittades inte",
"404-page-heading": "Hittades inte",
"new-404-page-title": "Sidan kunde inte hittas",
"new-404-page-heading": "Hoppsan. Sidan hittades inte.",
"404-img-text": "Hittades ej!",
"path-was-not-found": "Den begärda sökvägen hittades ej:",
"404-advice.p1": "Innehållet du letar efter kan fortfarande vara tillgängligt, men det kan finnas på en annan plats i ZIM-filen.",
"404-advice.p2": "Vänligen:",
"404-advice.p3": "Försök att använda sökfunktionen för att hitta det innehåll du vill ha",
"404-advice.p4": "Leta efter nyckelord eller titlar relaterade till den information du söker",
"404-advice.p5": "Den här metoden bör hjälpa dig att hitta önskat innehåll, även om den ursprungliga länken inte fungerar korrekt.",
"500-page-title": "Internt serverfel",
"500-page-heading": "Hoppsan. Sidan fungerar inte.",
"500-page-text": "Den begärda sökvägen kan inte levereras korrekt:",
"500-img-text": "Sidan fungerar ej",
"external-link-detected": "Extern länk upptäckt",
"caution-warning": "Varning!",
"external-link-intro": "Du är på väg att lämna Kiwix ZIM-läsare för att gå online till",
"external-link-advice.p1": "Länken du försöker komma åt är inte en del av ditt offlinepaket och kräver en internetanslutning.",
"external-link-advice.p2": "Om du kan gå online kan du försöka öppna länken.",
"external-link-advice.p3": "Du kan annars återgå till ditt ZIM-innehåll offline genom att använda webbläsarens bakåtknapp.",
"500-page-heading": "Internt serverfel",
"500-page-text": "Ett internt serverfel uppstod. Vi ber om ursäkt för det :/",
"fulltext-search-unavailable": "Fulltextsökning är inte tillgänglig",
"no-search-results": "Sökmaskinen för fulltext är inte tillgänglig för detta innehåll.",
"search-results-page-title": "Sök: {{SEARCH_PATTERN}}",
@@ -53,9 +36,9 @@
"search-result-book-info": "från {{BOOK_TITLE}}",
"word-count": "{{COUNT}} ord",
"library-button-text": "Gå till hemsidan",
"home-button-text": "Gå till huvudsidan för '{{{BOOK_TITLE}}}'",
"home-button-text": "Gå till huvudsidan för \"{{BOOK_TITLE}}\"",
"random-page-button-text": "Gå till en slumpmässigt utvald sida",
"searchbox-tooltip": "Sök '{{{BOOK_TITLE}}}'",
"searchbox-tooltip": "Sök efter \"{{BOOK_TITLE}}\"",
"confusion-of-tongues": "Två eller fler böcker på olika språk skulle delta i sökningen, vilket kan ge förvirrande resultat.",
"welcome-page-overzealous-filter": "Inga resultat. Vill du <a href=\"{{URL}}\">återställa filtret</a>?",
"powered-by-kiwix-html": "Drivs av&nbsp;<a href=\"https://kiwix.org\">Kiwix</a>",
@@ -90,6 +73,5 @@
"book-category.wikiversity": "Wikiversity",
"book-category.wikivoyage": "Wikivoyage",
"book-category.wiktionary": "Wiktionary",
"book-category.other": "Övriga",
"text-loading-content": "Laddar innehåll"
"book-category.other": "Övriga"
}

View File

@@ -22,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

@@ -2,8 +2,7 @@
"@metadata": {
"authors": [
"Hedda",
"Rofiatmustapha12",
"SaldırganSincap"
"Rofiatmustapha12"
]
},
"name": "Türkçe",
@@ -24,8 +23,8 @@
"404-page-title": "içerik bulunamadı",
"404-page-heading": "Bulunamadı",
"500-page-title": "İç Sunucu Hatası",
"500-page-heading": "Üzgünüz. Sayfa çalışmıyor.",
"500-page-text": "İstenen yol düzgün bir şekilde teslim edilemiyor:",
"500-page-heading": "İç Sunucu Hatası",
"500-page-text": "Dahili bir sunucu hatası oluştu. Bunun için üzgünüz :/",
"fulltext-search-unavailable": "Tam metin araması kullanılamıyor",
"no-search-results": "Tam metin arama motoru bu içerik için kullanılamaz.",
"search-results-page-title": "Arama: {{SEARCH_PATTERN}}",
@@ -34,9 +33,9 @@
"search-result-book-info": "{{BOOK_TITLE}} adlı kitaptan",
"word-count": "{{COUNT}} kelime",
"library-button-text": "Karşılama sayfasına git",
"home-button-text": "'{{{BOOK_TITLE}}}' ana sayfasına git",
"home-button-text": "'{{BOOK_TITLE}}' anasayfasına gidin",
"random-page-button-text": "Rastgele seçilen bir sayfaya git",
"searchbox-tooltip": "'{{{BOOK_TITLE}}}' ara",
"searchbox-tooltip": "'{{BOOK_TITLE}}' ara",
"confusion-of-tongues": "Aramaya farklı dillerde iki veya daha fazla kitap katılacak ve bu da kafa karıştırıcı sonuçlara yol açabilecektir.",
"welcome-page-overzealous-filter": "Sonuç yok. <a href=\"{{URL}}\">Filtreyi sıfırlamak</a> ister misiniz?",
"powered-by-kiwix-html": "<a href=\"https://kiwix.org\">Kiwix</a> tarafından desteklenmektedir",
@@ -46,16 +45,16 @@
"count-of-matching-books": "{{COUNT}} kitap",
"download": "İndir",
"direct-download-link-text": "Doğrudan",
"direct-download-alt-text": "Doğrudan HTTP(S) üzerinden indir",
"hash-download-link-text": "SHA-256 sağlama toplamı",
"hash-download-alt-text": "SHA-256 dosya toplam kontrolünü görüntüle",
"direct-download-alt-text": "direkt indirme",
"hash-download-link-text": "Sha256 haşesi",
"hash-download-alt-text": "csv indir",
"magnet-link-text": "Mıknatıs bağlantısı",
"magnet-alt-text": "Mıknatıs bağlantısıyla indir",
"torrent-download-link-text": "BitTorrent",
"torrent-download-alt-text": "BitTorrent üzerinden indir",
"magnet-alt-text": "mıknatısı indir",
"torrent-download-link-text": "Hedef dosya",
"torrent-download-alt-text": "torrenti indir",
"library-opds-feed-all-entries": "Kütüphane OPDS Akışı - Tüm girişler",
"filter-by-tag": "\"{{{TAG}}}\" etiketine göre filtrele",
"stop-filtering-by-tag": "\"{{{TAG}}}\" etiketine göre filtrelemeyi durdur",
"filter-by-tag": "\"{{TAG}}\" etiketine göre filtrele",
"stop-filtering-by-tag": "\"{{TAG}}\" etiketine göre filtrelemeyi durdur",
"library-opds-feed-parameterised": "Kütüphane OPDS Özet Akışı - {{#LANG}}\nLanguage: {{LANG}} {{/LANG}}{{#CATEGORY}}\nCategory: {{CATEGORY}} {{/CATEGORY}} ile eşleşen girişler {{#TAG}}\nTag: {{TAG}} {{/TAG}}{{#Q}}\nQuery: {{Q}} {{/Q}}",
"welcome-to-kiwix-server": "Kiwix Sunucusuna Hoş Geldiniz",
"download-links-heading": "<b><i>{{BOOK_TITLE}}</i></b> için indirme bağlantıları",

View File

@@ -1,11 +1,9 @@
{
"@metadata": {
"authors": [
"Cyanjiang",
"GuoPC",
"IceButBin",
"Kichin",
"Peterxy12",
"Prmsh",
"StarrySky",
"Sunai",
@@ -30,25 +28,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}}",
@@ -57,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

@@ -23,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}}",
@@ -50,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> 提供技術支援",
@@ -69,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,20 +269,11 @@ function translateErrorPageIfNeeded() {
translatePageInWindow(contentIframe.contentWindow);
}
let iframeLocationHref = null;
function handle_content_url_change() {
const iframeLocation = contentIframe.contentWindow.location;
if ( iframeLocationHref == iframeLocation.href ||
!iframeLocation.pathname.startsWith(root + '/content/') )
return;
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));
@@ -358,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) ) {
@@ -415,32 +402,25 @@ 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() {
const loader = document.getElementById("kiwix__loader");
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",
@@ -588,7 +572,6 @@ function setupViewer() {
const kiwixToolBarWrapper = document.getElementById('kiwixtoolbarwrapper');
if ( ! viewerSettings.toolbarEnabled ) {
finishViewerSetup();
return;
}
@@ -637,13 +620,10 @@ function updateUIText() {
function finishViewerSetupOnceTranslationsAreLoaded()
{
updateUIText();
finishViewerSetup();
}
function finishViewerSetup()
{
handle_location_hash_change();
window.onhashchange = handle_location_hash_change;
window.onpopstate = handle_history_state_change;
viewerSetupComplete = true;
}

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

@@ -117,10 +117,10 @@
{{title}}
</a>
{{#snippet}}
<cite>{{{snippet}}}...</cite>
<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

@@ -21,7 +21,7 @@
<title>{{title}}</title>
<link>{{absolutePath}}</link>
{{#snippet}}
<description>{{{snippet}}}...</description>
<description>{{>snippet}}...</description>
{{/snippet}}
{{#bookTitle}}
<book>

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=00e0fdf3" },
{ 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=00e0fdf3" 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));
}