Compare commits

..

4 Commits

Author SHA1 Message Date
Matthieu Gautier
45b3cfca16 Merge pull request #999 from kiwix/const-abi 2023-09-14 13:42:43 +02:00
Kunal Mehta
65b3c8442f Revert ABI breakage in kiwix::Downloader::getDownloadIds()
Changing it to be `const` is an ABI change since the symbol changes from
`_ZN5...` to `_ZNK5...` (addition of "K").

The other changes made in 18b7b5f27 are probably fine to keep since they
don't appear to be used from what I can tell.

Fixes https://github.com/kiwix/libkiwix/issues/998.
2023-09-14 13:20:41 +02:00
Matthieu Gautier
e461f2b437 New version 12.1.1 2023-07-31 16:50:30 +02:00
Matthieu Gautier
313ecc3f19 Revert "Make Downloader return shared_ptr instead of raw pointer."
This reverts commit 0e612de4d1.
2023-07-27 15:31:07 +02:00
163 changed files with 2313 additions and 9614 deletions

View File

@@ -8,193 +8,126 @@ on:
jobs:
macOS:
strategy:
fail-fast: false
matrix:
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
- target: ios-x86_64-dyn
arch_name: x86-apple-ios-simulator
run_test: false
runs-on: macos-15
runs-on: macos-11
env:
HOME: /Users/runner
steps:
- name: Retrieve source code
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Install packages
run: |
brew update
brew install ninja meson
brew unlink python3
# upgrade from python@3.11.2_1 to python@3.11.3 fails to overwrite those
rm -f /usr/local/bin/2to3 /usr/local/bin/2to3-3.11 /usr/local/bin/idle3 /usr/local/bin/idle3.11 /usr/local/bin/pydoc3 /usr/local/bin/pydoc3.11 /usr/local/bin/python3 /usr/local/bin/python3-config /usr/local/bin/python3.11 /usr/local/bin/python3.11-config
brew install pkg-config ninja meson
- name: Install dependencies
uses: kiwix/kiwix-build/actions/dl_deps_archive@main
with:
target_platform: ${{ matrix.target }}
- name: Compile
env:
PKG_CONFIG_PATH: ${{env.HOME}}/BUILD_${{matrix.arch_name}}/INSTALL/lib/pkgconfig
CPPFLAGS: -I${{env.HOME}}/BUILD_${{matrix.arch_name}}/INSTALL/include
MESON_OPTION: --default-library=shared -Db_coverage=true
MESON_CROSSFILE: ${{env.HOME}}/BUILD_${{matrix.arch_name}}/meson_cross_file.txt
shell: bash
ARCHIVE_NAME: deps2_macos_native_dyn_libkiwix.tar.xz
run: |
if [ -e $MESON_CROSSFILE ]; then
MESON_OPTION="$MESON_OPTION --cross-file $MESON_CROSSFILE -Dstatic-linkage=true"
fi
meson . build ${MESON_OPTION}
wget -O- https://tmp.kiwix.org/ci/${{env.ARCHIVE_NAME}} | tar -xJ -C ${{env.HOME}}
- name: Compile source code
env:
PKG_CONFIG_PATH: ${{env.HOME}}/BUILD_native_dyn/INSTALL/lib/pkgconfig
CPPFLAGS: -I${{env.HOME}}/BUILD_native_dyn/INSTALL/include
run: |
meson . build --default-library=shared -Db_coverage=true
ninja -C build
- name: Test libkiwix
if: matrix.run_test
env:
SKIP_BIG_MEMORY_TEST: 1
LD_LIBRARY_PATH: ${{env.HOME}}/BUILD_${{matrix.arch_name}}/INSTALL/lib:${{env.HOME}}/BUILD_${{matrix.arch_name}}/INSTALL/lib64
LD_LIBRARY_PATH: ${{env.HOME}}/BUILD_native_dyn/INSTALL/lib:${{env.HOME}}/BUILD_native_dyn/INSTALL/lib64
run: meson test -C build --verbose
Windows:
runs-on: windows-2022
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup python 3.10
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Install packages
run:
choco install pkgconfiglite ninja
- name: Install python modules
run: pip3 install meson
- name: Setup MSVC compiler
uses: bus1/cabuild/action/msdevshell@v1
with:
architecture: x64
- name: Install dependencies
uses: kiwix/kiwix-build/actions/dl_deps_archive@main
with:
target_platform: win-x86_64-static
- name: Compile
shell: cmd
run: |
set PKG_CONFIG_PATH=%cd%\BUILD_win-amd64\INSTALL\lib\pkgconfig
set CPPFLAGS=-I%cd%\BUILD_win-amd64\INSTALL\include
meson.exe setup . build -Dwerror=false --default-library=static --buildtype=release
cd build
ninja.exe
- name: Test
shell: cmd
run: |
cd build
meson.exe test --verbose
env:
WAIT_TIME_FACTOR_TEST: 10
Linux:
strategy:
fail-fast: false
matrix:
target:
- linux-x86_64-static
- linux-x86_64-dyn
- android-arm
- android-arm64
image_variant: ['jammy']
name:
- native_static
- native_dyn
- android_arm
- android_arm64
- win32_static
- win32_dyn
include:
- target: linux-x86_64-static
- name: native_static
target: native_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
- name: native_dyn
target: native_dyn
image_variant: focal
lib_postfix: '/x86_64-linux-gnu'
arch_name: linux-x86_64
run_test: true
coverage: true
- target: android-arm
- name: android_arm
target: android_arm
image_variant: focal
lib_postfix: '/arm-linux-androideabi'
arch_name: arm-linux-androideabi
run_test: false
coverage: false
- target: android-arm64
- name: android_arm64
target: android_arm64
image_variant: focal
lib_postfix: '/aarch64-linux-android'
arch_name: aarch64-linux-android
run_test: false
coverage: false
- name: win32_static
target: win32_static
image_variant: f35
lib_postfix: '64'
- name: win32_dyn
target: win32_dyn
image_variant: f35
lib_postfix: '64'
env:
HOME: /home/runner
runs-on: ubuntu-22.04
runs-on: ubuntu-20.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}}:37"
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install dependencies
uses: kiwix/kiwix-build/actions/dl_deps_archive@main
with:
target_platform: ${{ matrix.target }}
uses: actions/checkout@v3
- name: Install deps
shell: bash
run: |
ARCHIVE_NAME=deps2_${OS_NAME}_${{matrix.target}}_libkiwix.tar.xz
wget -O- http://tmp.kiwix.org/ci/${ARCHIVE_NAME} | tar -xJ -C /home/runner
- name: Compile
shell: bash
run: |
meson --version
if [[ "${{matrix.target}}" =~ .*-dyn ]]; then
if [[ "${{matrix.target}}" =~ .*_dyn ]]; then
MESON_OPTION="--default-library=shared"
else
MESON_OPTION="--default-library=static"
fi
if [ -e "${{env.HOME}}/BUILD_${{matrix.arch_name}}/meson_cross_file.txt" ]; then
MESON_OPTION="$MESON_OPTION --cross-file ${{env.HOME}}/BUILD_${{matrix.arch_name}}/meson_cross_file.txt"
else
if [[ "${{matrix.target}}" =~ native_.* ]]; then
MESON_OPTION="$MESON_OPTION -Db_coverage=true"
else
MESON_OPTION="$MESON_OPTION --cross-file $HOME/BUILD_${{matrix.target}}/meson_cross_file.txt"
fi
if [[ "${{matrix.target}}" =~ android-.* ]]; then
if [[ "${{matrix.target}}" =~ android_.* ]]; then
MESON_OPTION="$MESON_OPTION -Dstatic-linkage=true"
fi
meson . build ${MESON_OPTION}
cd build
ninja
env:
PKG_CONFIG_PATH: "/home/runner/BUILD_${{matrix.arch_name}}/INSTALL/lib/pkgconfig:/home/runner/BUILD_${{matrix.arch_name}}/INSTALL/lib${{matrix.lib_postfix}}/pkgconfig"
CPPFLAGS: "-I/home/runner/BUILD_${{matrix.arch_name}}/INSTALL/include"
PKG_CONFIG_PATH: "/home/runner/BUILD_${{matrix.target}}/INSTALL/lib/pkgconfig:/home/runner/BUILD_${{matrix.target}}/INSTALL/lib${{matrix.lib_postfix}}/pkgconfig"
CPPFLAGS: "-I/home/runner/BUILD_${{matrix.target}}/INSTALL/include"
- name: Test
if: matrix.run_test
if: startsWith(matrix.target, 'native_')
shell: bash
run: |
cd build
meson test --verbose
if [[ "${{matrix.coverage}}" = "true" ]]; then
ninja coverage
fi
ninja coverage
env:
LD_LIBRARY_PATH: "/home/runner/BUILD_${{matrix.arch_name}}/INSTALL/lib:/home/runner/BUILD_${{matrix.arch_name}}/INSTALL/lib${{matrix.lib_postfix}}"
LD_LIBRARY_PATH: "/home/runner/BUILD_${{matrix.target}}/INSTALL/lib:/home/runner/BUILD_${{matrix.target}}/INSTALL/lib${{matrix.lib_postfix}}"
SKIP_BIG_MEMORY_TEST: 1
- name: Publish coverage
if: matrix.coverage
if: startsWith(matrix.target, 'native_')
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}

View File

@@ -5,25 +5,19 @@ on:
push:
branches:
- main
release:
types: [published]
jobs:
build-deb:
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
distro:
# - debian-unstable
# - debian-trixie
# - debian-bookworm
# - debian-bullseye
- ubuntu-noble
- ubuntu-kinetic
- ubuntu-jammy
- ubuntu-focal
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
# Determine which PPA we should upload to
- name: PPA
@@ -31,51 +25,23 @@ jobs:
run: |
if [[ $REF == refs/tags* ]]
then
echo "ppa=kiwixteam/release" >> $GITHUB_OUTPUT
echo "::set-output name=ppa::kiwixteam/release"
else
echo "ppa=kiwixteam/dev" >> $GITHUB_OUTPUT
echo "::set-output name=ppa::kiwixteam/dev"
fi
env:
REF: ${{ github.ref }}
- uses: legoktm/gh-action-auto-dch@main
- uses: legoktm/gh-action-auto-dch@master
with:
fullname: Kiwix builder
email: release+launchpad@kiwix.org
distro: ${{ matrix.distro }}
# - uses: legoktm/gh-action-build-deb@debian-unstable
# if: matrix.distro == 'debian-unstable'
# name: Build package for debian-unstable
# id: build-debian-unstable
# with:
# args: --no-sign
#
# - uses: legoktm/gh-action-build-deb@b47978ba8498dc8b8153cc3b5f99a5fc1afa5de1 # pin@debian-trixie
# if: matrix.distro == 'debian-trixie'
# name: Build package for debian-trixie
# id: build-debian-trixie
# with:
# args: --no-sign
#
# - uses: legoktm/gh-action-build-deb@1f4e86a6bb34aaad388167eaf5eb85d553935336 # pin@debian-bookworm
# if: matrix.distro == 'debian-bookworm'
# name: Build package for debian-bookworm
# id: build-debian-bookworm
# with:
# args: --no-sign
#
# - uses: legoktm/gh-action-build-deb@084b4263209252ec80a75d2c78a586192c17f18d # pin@debian-bullseye
# if: matrix.distro == 'debian-bullseye'
# name: Build package for debian-bullseye
# id: build-debian-bullseye
# with:
# args: --no-sign
- uses: legoktm/gh-action-build-deb@9114a536498b65c40b932209b9833aa942bf108d # pin@ubuntu-noble
if: matrix.distro == 'ubuntu-noble'
name: Build package for ubuntu-noble
id: build-ubuntu-noble
- uses: legoktm/gh-action-build-deb@ubuntu-kinetic
if: matrix.distro == 'ubuntu-kinetic'
name: Build package for ubuntu-kinetic
id: build-ubuntu-kinetic
with:
args: --no-sign
ppa: ${{ steps.ppa.outputs.ppa }}
@@ -88,24 +54,34 @@ jobs:
args: --no-sign
ppa: ${{ steps.ppa.outputs.ppa }}
- uses: actions/upload-artifact@v4
- 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@v3
with:
name: Packages for ${{ matrix.distro }}
path: output
- uses: legoktm/gh-action-dput@main
- uses: legoktm/gh-action-dput@master
name: Upload dev package
# Only upload on pushes to main
# Only upload on pushes to git default branch
if: github.event_name == 'push' && github.event.ref == 'refs/heads/main' && startswith(matrix.distro, 'ubuntu-')
with:
gpg_key: ${{ secrets.LAUNCHPAD_GPG }}
repository: ppa:kiwixteam/dev
packages: output/*_source.changes
- uses: legoktm/gh-action-dput@main
- uses: legoktm/gh-action-dput@master
name: Upload release package
if: github.event_name == 'release' && startswith(matrix.distro, 'ubuntu-')
# Only upload on pushes to master or tag
if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') && startswith(matrix.distro, 'ubuntu-')
with:
gpg_key: ${{ secrets.LAUNCHPAD_GPG }}
repository: ppa:kiwixteam/release
packages: output/*_source.changes

View File

@@ -1,89 +1,7 @@
libkiwix 14.1.1
libkiwix 12.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
===============
* Server:
- Support of IPv6 (@veloman-yunkan @aryanA101a #1074 #1093)
- Better public IP configuration/detection (@sgourdas #1132)
- Fix API errors in catalog searches if Xapian keyword in used (@veloman-yunkan #1137)
- Clearly define which Web browsers are supported (@kelson42 @rgaudin @jaifroid @benoit74 #1132)
- Improve welcome page download buttons (@veloman-yunkan #1094)
- Better handling of external (non-HTTP) links (@veloman-yunkan #1123)
- Fix book illustration size on welcome page to 48x48 pixels (@veloman-yunkan #1127)
- Remove "Multiple Languages" in language filter (@veloman-yunkan #1098)
- Stop transforming tags casing (@kelson42 @veloman-yunkan #1079 #1121)
- ZIM file size consistently advertised in MiB (@harsha-mangena #1132)
- Few new supported languages in the filter (@kelson42 #1080)
- Improve accesskeys (@kelson42 #1075)
- Add OpenSearch <link> to head of pages (@kelson42 #1070)
* Compilation/Packaging:
- Multiple fixes around deb packaging (@kelson42 #1108 #1114 #1135)
- Generating of libkiwix.pc via Meson (@veloman-yunkan #1133)
- Native Windows CI/CD (@mgautierfr @kelson42 #1113 #1125)
- Better check (maximum) libzim version (@kelson42 #1124)
- Multiple automated tests improvements (@veloman-yunkan #1068 #1067)
* Other:
- Deleted supported env. variable `$KIWIX_DATA_DIR` and `kiwix::getDataDirectory()` (@sgourdas #1107)
- New string slugification for filenames (@shaopenglin #1105)
- Multiple improvements around aria2c download mgmt. (@veloman-yunkan #1097)
libkiwix 13.1.0
===============
* Server:
- Properly translated error pages (@veloman-yunkan #1032)
- Properly translated search result page (@veloman-yunkan #1046)
- Default UI language is resolved in frontend (@veloman-yunkan #1044)
- Better support of older Web browsers by polyfilling replaceAll() (@veloman-yunkan #1054)
* New API to migrate bookmarks between books (@mgautierfr #1043)
* Fixed compilation on Haiku OS (@Begasus #1048)
libkiwix 13.0.0
===============
* Server:
- Improved look & feel of kiwix-serve UI (@veloman-yunkan #917 #1021)
- Increase tolerance to malformed (control characters) ZIM entry titles (@veloman-yunkan #1023)
- API allowing to filter many categories at once (@juuz0 #974)
- Cookie-less user language control (@veloman-yumkan #997)
- Hack to fix Mirrorbrain based broken magnet URLs (@rgaudin #1001)
* Fix handling of books with 'Name' metadata with dots (@mgautier #1016)
* New method beautifyFileSize() to provide nice-looking book sizes (@vuuz0 #971)
* Fix a few missing includes (@mgautierfr #978)
* New functions to read - kiwix-serve - languages and categories streams (@juuz0 #967)
* Add support of Fon language (@kelson42 #1013)
* C++17 code base compliancy (@mgautierfr #996)
* Use everywhere std::shared_ptr in place of raw pointer (@mgautierfr #991)
* Do not use [[nodiscard]] attribute on compiler not supporting it (@mgautierfr #1003)
* Add a non minified version of autoComplete.js (@mgautierfr #1008)
* Multiple CI/CD improvements (@kelson42 #982)
* Revert API break introduced in libkiwix 12.1.0
libkiwix 12.1.0
===============
@@ -120,6 +38,8 @@ libkiwix 12.1.0
* Remove libkiwix android publisher from the repository (@kelson42 #884)
* Various fixes of meson and CI. (@mgautierfr @kelson42)
libkiwix 12.0.0
===============
@@ -159,6 +79,7 @@ libkiwix 12.0.0
* Fix documentation (@kelson42 #816)
* Udpate translation (#787 #839 #847)
libkiwix 11.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)
@@ -24,9 +24,9 @@ with the Libkiwix compilation itself, we recommend to have a look to
Preamble
--------
Although the Libkiwix can be (cross-)compiled on/for many systems, the
Although the Libkiwix can be (cross-)compiled on/for many sytems, the
following documentation explains how to do it on POSIX ones. It is
primarily thought for GNU/Linux systems and has been tested on recent
primarly thought for GNU/Linux systems and has been tested on recent
releases of Ubuntu and Fedora.
Dependencies
@@ -54,7 +54,7 @@ The following dependency needs to be available at runtime:
These dependencies may or may not be packaged by your operating
system. They may also be packaged but only in an older version. The
compilation script will tell you if one of them is missing or too old.
In the worst case, you will have to download and compile bleeding edge
In the worse case, you will have to download and compile bleeding edge
version by hand.
If you want to install these dependencies locally, then use the
@@ -201,7 +201,7 @@ To use JS provided by kiwix-serve you can use the following template to start wi
If you compile manually Libmicrohttpd, you might need to compile it
without GNU TLS, a bug here will impeach further compilation
without GNU TLS, a bug here will empeach further compilation
otherwise.
If the compilation still fails, you might need to get a more recent

17
debian/control vendored
View File

@@ -3,12 +3,13 @@ Priority: optional
Maintainer: Kiwix team <kiwix@kiwix.org>
Build-Depends: debhelper-compat (= 13),
meson,
pkgconf,
libzim-dev (>= 9.0), libzim-dev (<< 10.0),
pkg-config,
libzim-dev (>= 7.2.0~),
libcurl4-gnutls-dev,
libicu-dev,
libgtest-dev,
libkainjow-mustache-dev,
liblzma-dev,
libmicrohttpd-dev,
libpugixml-dev,
zlib1g-dev
@@ -21,13 +22,12 @@ Package: libkiwix-dev
Section: libdevel
Architecture: any
Multi-Arch: same
Depends: libkiwix14 (= ${binary:Version}), ${misc:Depends}, python3,
libzim-dev (>= 9.0), libzim-dev (<< 10.0),
Depends: libkiwix10 (= ${binary:Version}), ${misc:Depends}, python3,
libzim-dev (>= 7.2.0~),
libicu-dev,
libpugixml-dev,
libcurl4-gnutls-dev,
libmicrohttpd-dev,
zlib1g-dev
libmicrohttpd-dev
Description: library of common code for Kiwix (development)
Kiwix is an offline Wikipedia reader. libkiwix provides the
software core for Kiwix, and contains the code shared by all
@@ -35,12 +35,11 @@ Description: library of common code for Kiwix (development)
.
This package contains development files.
Package: libkiwix14
Package: libkiwix10
Architecture: any
Multi-Arch: same
Depends: ${shlibs:Depends}, ${misc:Depends}, aria2
Conflicts: libkiwix0, libkiwix3, libkiwix9, libkiwix10, libkiwix11, libkiwix12, libkiwix13
Replaces: libkiwix0, libkiwix3, libkiwix9, libkiwix10, libkiwix11, libkiwix12, libkiwix13
Conflicts: libkiwix0, libkiwix3, libkiwix9
Description: library of common code for Kiwix
Kiwix is an offline Wikipedia reader. libkiwix provides the
software core for Kiwix, and contains the code shared by all

View File

View File

@@ -24,6 +24,8 @@ author = 'libkiwix-team'
# -- General configuration ---------------------------------------------------
on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
@@ -40,7 +42,9 @@ templates_path = ['_templates']
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
html_theme = 'sphinx_rtd_theme'
if not on_rtd:
html_theme = 'sphinx_rtd_theme'
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,

View File

@@ -1,3 +1,2 @@
breathe
exhale
sphinx_rtd_theme

View File

@@ -29,33 +29,19 @@ class xml_node;
namespace kiwix
{
class Book;
/**
* A class to store information about a bookmark (an article in a book)
*/
class Bookmark
{
public:
/**
* Create an empty bookmark.
*
* Bookmark must be populated with `set*` methods
*/
Bookmark();
/**
* Create a bookmark given a Book, a path and a title.
*/
Bookmark(const Book& book, const std::string& path, const std::string& title);
~Bookmark();
void updateFromXml(const pugi::xml_node& node);
const std::string& getBookId() const { return m_bookId; }
const std::string& getBookTitle() const { return m_bookTitle; }
const std::string& getBookName() const { return m_bookName; }
const std::string& getBookFlavour() const { return m_bookFlavour; }
const std::string& getUrl() const { return m_url; }
const std::string& getTitle() const { return m_title; }
const std::string& getLanguage() const { return m_language; }
@@ -63,8 +49,6 @@ class Bookmark
void setBookId(const std::string& bookId) { m_bookId = bookId; }
void setBookTitle(const std::string& bookTitle) { m_bookTitle = bookTitle; }
void setBookName(const std::string& bookName) { m_bookName = bookName; }
void setBookFlavour(const std::string& bookFlavour) { m_bookFlavour = bookFlavour; }
void setUrl(const std::string& url) { m_url = url; }
void setTitle(const std::string& title) { m_title = title; }
void setLanguage(const std::string& language) { m_language = language; }
@@ -73,8 +57,6 @@ class Bookmark
protected:
std::string m_bookId;
std::string m_bookTitle;
std::string m_bookName;
std::string m_bookFlavour;
std::string m_url;
std::string m_title;
std::string m_language;

View File

@@ -16,7 +16,6 @@
namespace kiwix {
enum class IpMode { IPV4, IPV6, ALL, AUTO }; // AUTO: Server decides the protocol
typedef zim::size_type size_type;
typedef zim::offset_type offset_type;

View File

@@ -168,16 +168,8 @@ class Download {
*/
class Downloader
{
public: // types
typedef std::vector<std::pair<std::string, std::string>> Options;
public: // functions
/*
* Create a new Downloader object.
*
* @param sessionFileDir: The directory where aria2 will store its session file.
*/
explicit Downloader(std::string sessionFileDir);
public:
Downloader();
virtual ~Downloader();
void close();
@@ -185,22 +177,14 @@ class Downloader
/**
* Start a new download.
*
* This method is thread safe and returns a pointer to a newly created
* `Download` or an existing one with a matching URI. In the latter case
* the options parameter is ignored, which can lead to surprising results.
* For example, if the old and new download requests (sharing the same URI)
* have different values for the download directory or output file name
* options, after the download is reported to be complete the downloaded file
* will be present only at the location specified for the first request.
*
* This method is thread safe and return a pointer to a newly created `Download`.
* User should call `update` on the returned `Download` to have an accurate status.
*
* @param uri: The uri of the thing to download.
* @param downloadDir: The download directory where the thing should be stored (takes precedence over any "dir" in `options`).
* @param options: A series of pair <option_name, option_value> to pass to aria.
* @return: The newly created Download.
*/
std::shared_ptr<Download> startDownload(const std::string& uri, const std::string& downloadDir, Options options = {});
Download* startDownload(const std::string& uri, const std::vector<std::pair<std::string, std::string>>& options = {});
/**
* Get a download corrsponding to a download id (did)
@@ -210,7 +194,7 @@ class Downloader
* @return: The Download corresponding to did.
* @throw: Throw std::out_of_range if did is not found.
*/
std::shared_ptr<Download> getDownload(const std::string& did);
Download* getDownload(const std::string& did);
/**
* Get the number of downloads currently managed.
@@ -220,11 +204,11 @@ class Downloader
/**
* Get the ids of the managed downloads.
*/
std::vector<std::string> getDownloadIds() const;
std::vector<std::string> getDownloadIds();
private: // data
private:
mutable std::mutex m_lock;
std::map<std::string, std::shared_ptr<Download>> m_knownDownloads;
std::map<std::string, std::unique_ptr<Download>> m_knownDownloads;
std::shared_ptr<Aria2> mp_aria;
};
}

View File

View File

@@ -1,92 +0,0 @@
/*
* Copyright 2024 Veloman Yunkan <veloman.yunkan@gmail.com>
*
* 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_I18N
#define KIWIX_I18N
#include <map>
#include <string>
namespace kiwix
{
std::string getTranslatedString(const std::string& lang, const std::string& key);
namespace i18n
{
typedef std::map<std::string, std::string> Parameters;
std::string expandParameterizedString(const std::string& lang,
const std::string& key,
const Parameters& params);
class GetTranslatedString
{
public:
explicit GetTranslatedString(const std::string& lang) : m_lang(lang) {}
std::string operator()(const std::string& key) const
{
return getTranslatedString(m_lang, key);
}
std::string operator()(const std::string& key, const Parameters& params) const
{
return expandParameterizedString(m_lang, key, params);
}
private:
const std::string m_lang;
};
} // namespace i18n
class ParameterizedMessage
{
public: // types
typedef i18n::Parameters Parameters;
public: // functions
ParameterizedMessage(const std::string& msgId, const Parameters& params)
: msgId(msgId)
, params(params)
{}
std::string getText(const std::string& lang) const;
const std::string& getMsgId() const { return msgId; }
const Parameters& getParams() const { return params; }
private: // data
const std::string msgId;
const Parameters params;
};
inline ParameterizedMessage nonParameterizedMessage(const std::string& msgId)
{
const ParameterizedMessage::Parameters noParams;
return ParameterizedMessage(msgId, noParams);
}
std::string translateBookCategory(const std::string& lang, const std::string& category);
} // namespace kiwix
#endif // KIWIX_I18N

View File

@@ -34,10 +34,6 @@
#define KIWIX_LIBRARY_VERSION "20110515"
namespace Xapian {
class WritableDatabase;
};
namespace kiwix
{
@@ -55,22 +51,6 @@ enum supportedListMode {
NOVALID = 1 << 5
};
enum MigrationMode {
/** When migrating bookmarks, do not allow to migrate to an older book than the currently pointed one
* (or date stored in the bookmark if book is invalid)
*
* If no newer books are found, no upgrade is made.
*/
UPGRADE_ONLY = 0,
/** Try hard to do a migration. This mostly does:
* - Try to find a newer book.
* - If book is invalid: find a best book, potentially older.
* Older book will never be returned if current book is a valid one.
*/
ALLOW_DOWNGRADE = 1,
};
class Filter {
public: // types
using Tags = std::vector<std::string>;
@@ -87,7 +67,6 @@ class Filter {
std::string _query;
bool _queryIsPartial;
std::string _name;
std::string _flavour;
public: // functions
Filter();
@@ -126,12 +105,6 @@ class Filter {
Filter& acceptTags(const Tags& tags);
Filter& rejectTags(const Tags& tags);
/**
* Set the filter to only accept books in the specified category.
*
* Multiple categories can be specified as a comma-separated list (in
* which case a book in any of those categories will match).
*/
Filter& category(std::string category);
/**
@@ -147,7 +120,6 @@ class Filter {
Filter& maxSize(size_t size);
Filter& query(std::string query, bool partial=true);
Filter& name(std::string name);
Filter& flavour(std::string flavour);
Filter& clearLang();
Filter& clearCategory();
@@ -170,9 +142,6 @@ class Filter {
bool hasCreator() const;
const std::string& getCreator() const { return _creator; }
bool hasFlavour() const;
const std::string& getFlavour() const { return _flavour; }
const Tags& getAcceptTags() const { return _acceptTags; }
const Tags& getRejectTags() const { return _rejectTags; }
@@ -198,53 +167,31 @@ class ZimSearcher : public zim::Searcher
std::mutex m_mutex;
};
template<typename, typename>
class ConcurrentCache;
template<typename, typename>
class MultiKeyCache;
using LibraryPtr = std::shared_ptr<Library>;
using ConstLibraryPtr = std::shared_ptr<const Library>;
// Some compiler we use don't have [[nodiscard]] attribute.
// We don't want to declare `create` with it in this case.
#define LIBKIWIX_NODISCARD
#if defined __has_cpp_attribute
#if __has_cpp_attribute (nodiscard)
#undef LIBKIWIX_NODISCARD
#define LIBKIWIX_NODISCARD [[nodiscard]]
#endif
#endif
/**
* A Library store several books.
*/
class Library: public std::enable_shared_from_this<Library>
class Library
{
// all data fields must be added in LibraryBase
mutable std::mutex m_mutex;
public:
typedef uint64_t Revision;
typedef std::vector<std::string> BookIdCollection;
typedef std::map<std::string, int> AttributeCounts;
typedef std::set<std::string> BookIdSet;
private:
Library();
public:
LIBKIWIX_NODISCARD static LibraryPtr create() {
return LibraryPtr(new Library());
}
Library();
~Library();
/**
* Library is not a copiable object. However it can be moved.
*/
Library(const Library& ) = delete;
Library(Library&& ) = delete;
Library(Library&& );
void operator=(const Library& ) = delete;
Library& operator=(Library&& ) = delete;
Library& operator=(Library&& );
/**
* Add a book to the library.
@@ -271,7 +218,7 @@ class Library: public std::enable_shared_from_this<Library>
void addBookmark(const Bookmark& bookmark);
/**
* Remove a bookmark
* Remove a bookmarkk
*
* @param zimId The zimId of the bookmark.
* @param url The url of the bookmark.
@@ -279,66 +226,6 @@ class Library: public std::enable_shared_from_this<Library>
*/
bool removeBookmark(const std::string& zimId, const std::string& url);
/**
* Migrate all invalid bookmarks.
*
* All invalid bookmarks (ie pointing to unknown books, no check is made on bookmark pointing to
* invalid articles of valid book) will be migrated (if possible) to a better book.
* "Better book", will be determined using method `getBestTargetBookId`.
*
* @return A tuple<int, int>: <The number of bookmarks updated>, <Number of invalid bookmarks before migration was performed>.
*/
std::tuple<int, int> migrateBookmarks(MigrationMode migrationMode = ALLOW_DOWNGRADE);
/**
* Migrate all bookmarks associated to a specific book.
*
* All bookmarks associated to `sourceBookId` book will be migrated to a better book.
* "Better book", will be determined using method `getBestTargetBookId`.
*
* @param sourceBookId the source bookId of the bookmarks to migrate.
* @param migrationMode how we will find the best book.
* @return The number of bookmarks updated.
*/
int migrateBookmarks(const std::string& sourceBookId, MigrationMode migrationMode = UPGRADE_ONLY);
/**
* Migrate bookmarks
*
* Migrate all bookmarks pointing to `source` to `destination`.
*
* @param sourceBookId the source bookId of the bookmarks to migrate.
* @param targetBookId the destination bookId to migrate the bookmarks to.
* @return The number of bookmarks updated.
*/
int migrateBookmarks(const std::string& sourceBookId, const std::string& targetBookId);
/**
* Get the best available bookId for a bookmark.
*
* Given a bookmark, return the best available bookId.
* "best available bookId" is determined using heuristitcs based on book name, flavour and date.
*
* @param bookmark The bookmark to search the bookId for.
* @param migrationMode The migration mode to use.
* @return A bookId. Potentially empty string if no suitable book found.
*/
std::string getBestTargetBookId(const Bookmark& bookmark, MigrationMode migrationMode) const;
/**
* Get the best bookId for a combination of book's name, flavour and date.
*
* Given a bookName (mandatory), try to find the best book.
* If preferedFlavour is given, will try to find a book with the same flavour. If not found, return a book with a different flavour.
* If minDate is given, return a book newer than minDate. If not found, return a empty bookId.
*
* @param bookName The name of the book
* @param preferedFlavour The prefered flavour.
* @param minDate the minimal book date acceptable. Must be a string in the format "YYYY-MM-DD".
* @return A bookId corresponding to the query, or empty string if not found.
*/
std::string getBestTargetBookId(const std::string& bookName, const std::string& preferedFlavour="", const std::string& minDate="") const;
// XXX: This is a non-thread-safe operation
const Book& getBookById(const std::string& id) const;
// XXX: This is a non-thread-safe operation
@@ -475,37 +362,19 @@ class Library: public std::enable_shared_from_this<Library>
private: // types
typedef const std::string& (Book::*BookStrPropMemFn)() const;
struct Entry : Book
{
Library::Revision lastUpdatedRevision = 0;
};
struct Impl;
private: // functions
AttributeCounts getBookAttributeCounts(BookStrPropMemFn p) const;
std::vector<std::string> getBookPropValueSet(BookStrPropMemFn p) const;
BookIdCollection filterViaBookDB(const Filter& filter) const;
std::string getBestFromBookCollection(BookIdCollection books, const Bookmark& bookmark, MigrationMode migrationMode) const;
unsigned int getBookCount_not_protected(const bool localBooks, const bool remoteBooks) const;
void updateBookDB(const Book& book);
void dropCache(const std::string& bookId);
private: //data
mutable std::recursive_mutex m_mutex;
Library::Revision m_revision;
std::map<std::string, Entry> m_books;
using ArchiveCache = ConcurrentCache<std::string, std::shared_ptr<zim::Archive>>;
std::unique_ptr<ArchiveCache> mp_archiveCache;
using SearcherCache = MultiKeyCache<std::string, std::shared_ptr<ZimSearcher>>;
std::unique_ptr<SearcherCache> mp_searcherCache;
std::vector<kiwix::Bookmark> m_bookmarks;
std::unique_ptr<Xapian::WritableDatabase> m_bookDB;
std::unique_ptr<Impl> mp_impl;
};
// We don't need it anymore and we don't want to polute any other potential usage
// of `LIBKIWIX_NODISCARD` token.
#undef LIBKIWIX_NODISCARD
}
#endif

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

@@ -37,10 +37,10 @@ namespace kiwix
class LibraryManipulator
{
public: // functions
explicit LibraryManipulator(LibraryPtr library);
explicit LibraryManipulator(Library* library);
virtual ~LibraryManipulator();
LibraryPtr getLibrary() const { return library; }
Library& getLibrary() const { return library; }
bool addBookToLibrary(const Book& book);
void addBookmarkToLibrary(const Bookmark& bookmark);
@@ -52,7 +52,7 @@ class LibraryManipulator
virtual void booksWereRemovedFromLibrary();
private: // data
LibraryPtr library;
kiwix::Library& library;
};
/**
@@ -64,8 +64,8 @@ class Manager
typedef std::vector<std::string> Paths;
public: // functions
explicit Manager(LibraryManipulator manipulator);
explicit Manager(LibraryPtr library);
explicit Manager(LibraryManipulator* manipulator);
explicit Manager(Library* library);
/**
* Read a `library.xml` and add book in the file to the library.
@@ -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;
@@ -172,7 +163,7 @@ class Manager
uint64_t m_itemsPerPage = 0;
protected:
kiwix::LibraryManipulator manipulator;
std::shared_ptr<kiwix::LibraryManipulator> manipulator;
bool readBookFromPath(const std::string& path, Book* book);
bool parseXmlDom(const pugi::xml_document& doc,

View File

@@ -4,15 +4,15 @@ headers = [
'common.h',
'library.h',
'manager.h',
'libxml_dumper.h',
'opds_dumper.h',
'downloader.h',
'search_renderer.h',
'server.h',
'spelling_correction.h',
'kiwixserve.h',
'name_mapper.h',
'tools.h',
'version.h',
'i18n.h'
'version.h'
]
install_headers(headers, subdir:'kiwix')

View File

@@ -50,19 +50,16 @@ 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;
private:
void mapName(const kiwix::Library& lib, std::string name, std::string id);
};
class UpdatableNameMapper : public NameMapper {
typedef std::shared_ptr<NameMapper> NameMapperHandle;
public:
UpdatableNameMapper(std::shared_ptr<Library> library, bool withAlias);
UpdatableNameMapper(Library& library, bool withAlias);
virtual std::string getNameForId(const std::string& id) const;
virtual std::string getIdForName(const std::string& name) const;
@@ -74,7 +71,7 @@ class UpdatableNameMapper : public NameMapper {
private:
mutable std::mutex mutex;
std::shared_ptr<Library> library;
Library& library;
NameMapperHandle nameMapper;
const bool withAlias;
};

View File

View File

@@ -37,11 +37,29 @@ class SearchRenderer
/**
* Construct a SearchRenderer from a SearchResultSet.
*
* The constructed version of the SearchRenderer will not introduce
* the book name for each result. It is better to use the other constructor
* with a Library pointer to have a better html page.
*
* @param srs The `SearchResultSet` to render.
* @param mapper The `NameMapper` to use to do the rendering.
* @param start The start offset used for the srs.
* @param estimatedResultCount The estimatedResultCount of the whole search
*/
SearchRenderer(zim::SearchResultSet srs, unsigned int start, unsigned int estimatedResultCount);
SearchRenderer(zim::SearchResultSet srs, NameMapper* mapper,
unsigned int start, unsigned int estimatedResultCount);
/**
* Construct a SearchRenderer from a SearchResultSet.
*
* @param srs The `SearchResultSet` to render.
* @param mapper The `NameMapper` to use to do the rendering.
* @param library The `Library` to use to look up book details for search results.
* @param start The start offset used for the srs.
* @param estimatedResultCount The estimatedResultCount of the whole search
*/
SearchRenderer(zim::SearchResultSet srs, NameMapper* mapper, Library* library,
unsigned int start, unsigned int estimatedResultCount);
~SearchRenderer();
@@ -72,39 +90,24 @@ class SearchRenderer
this->pageLength = pageLength;
}
/**
* set user language
*/
void setUserLang(const std::string& lang){
this->userlang = lang;
}
std::string renderTemplate(const std::string& tmpl_str);
/**
* Generate the html page with the resutls of the search.
*
* @param mapper The `NameMapper` to use to do the rendering.
* @param library The `Library` to use to look up book details for search results.
May be nullptr. In this case, bookName is not set in the rendered string.
* @return The html string
*/
std::string getHtml(const NameMapper& mapper, const Library* library);
std::string getHtml();
/**
/**
* Generate the xml page with the resutls of the search.
*
* @param mapper The `NameMapper` to use to do the rendering.
* @param library The `Library` to use to look up book details for search results.
May be nullptr. In this case, bookName is not set in the rendered string.
* @return The xml string
*/
std::string getXml(const NameMapper& mapper, const Library* library);
std::string getXml();
protected: // function
std::string renderTemplate(const std::string& tmpl_str, const NameMapper& mapper, const Library *library);
protected:
std::string beautifyInteger(const unsigned int number);
zim::SearchResultSet m_srs;
NameMapper* mp_nameMapper;
Library* mp_library;
std::string searchBookQuery;
std::string searchPattern;
std::string protocolPrefix;
@@ -112,7 +115,6 @@ class SearchRenderer
unsigned int pageLength;
unsigned int estimatedResultCount;
unsigned int resultStart;
std::string userlang = "en";
};

View File

@@ -22,7 +22,6 @@
#include <string>
#include <memory>
#include "tools.h"
namespace kiwix
{
@@ -37,7 +36,7 @@ namespace kiwix
*
* @param library The library to serve.
*/
Server(std::shared_ptr<Library> library, std::shared_ptr<NameMapper> nameMapper=nullptr);
Server(Library* library, NameMapper* nameMapper=nullptr);
virtual ~Server();
@@ -52,7 +51,7 @@ namespace kiwix
void stop();
void setRoot(const std::string& root);
void setAddress(const std::string& addr);
void setAddress(const std::string& addr) { m_addr = addr; }
void setPort(int port) { m_port = port; }
void setNbThreads(int threads) { m_nbThreads = threads; }
void setMultiZimSearchLimit(unsigned int limit) { m_multizimSearchLimit = limit; }
@@ -63,19 +62,14 @@ 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;
int getPort();
std::string getAddress();
protected:
std::shared_ptr<Library> mp_library;
std::shared_ptr<NameMapper> mp_nameMapper;
Library* mp_library;
NameMapper* mp_nameMapper;
std::string m_root = "";
IpAddress m_addr;
std::string m_addr = "";
std::string m_indexTemplateString = "";
int m_port = 80;
int m_nbThreads = 1;
@@ -84,10 +78,7 @@ namespace kiwix
bool m_withTaskbar = true;
bool m_withLibraryButton = true;
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

@@ -23,21 +23,8 @@
#include <string>
#include <vector>
#include <map>
#include <cstdint>
#include "common.h"
namespace kiwix
{
struct IpAddress
{
std::string addr; // IPv4 address
std::string addr6; // IPv6 address
};
typedef std::pair<std::string, std::string> LangNameCodePair;
typedef std::vector<LangNameCodePair> FeedLanguages;
typedef std::vector<std::string> FeedCategories;
namespace kiwix {
/**
* Return the current directory.
@@ -46,6 +33,26 @@ typedef std::vector<std::string> FeedCategories;
*/
std::string getCurrentDirectory();
/**
* Return the data directory.
*
* The data directory is a directory where to put data (zim files, ...)
* It depends of the platform and it may be changed by user using environment variable.
*
* The resolution order is :
* - `KIWIX_DATA_DIR` env variable (if set).
* - On Windows :
* . `$APPDATA/kiwix` if $APPDATA is set
* . `$USERPROFILE/kiwix` if $USERPROFILE is set
* - Else :
* . `$XDG_DATA_HOME/kiwix`if $XDG_DATA_HOME is set
* . `$HOME/.local/share/kiwx` if $HOWE is set
* - current directory
*
* @return the path of the data directory (utf8 encoded)
*/
std::string getDataDirectory();
/** Return the path of the executable
*
* Some application may be packaged in auto extractible archive (Appimage) and the
@@ -199,73 +206,15 @@ bool fileReadable(const std::string& path);
std::string getMimeTypeForFile(const std::string& filename);
/** Provides all available network interfaces
*
* This function provides the available IPv4 and IPv6 network interfaces
* as a map from the interface name to its IPv4 and/or IPv6 address(es).
*/
std::map<std::string, IpAddress> getNetworkInterfacesIPv4Or6();
/** Provides all available IPv4 network interfaces
*
* This function provides the available IPv4 network interfaces
* as a map from the interface name to its IPv4 address.
*
* Provided for backward compatibility with libkiwix v13.1.0.
*/
std::map<std::string, std::string> getNetworkInterfaces();
/** Provides the best IP address
* This function provides the best IP addresses for both ipv4 and ipv6 protocols,
* in an IpAddress struct, based on the list given by getNetworkInterfacesIPv4Or6()
*/
IpAddress getBestPublicIps();
/** Provides the best IPv4 adddress
* Equivalent to getBestPublicIp(false). Provided for backward compatibility
* with libkiwix v13.1.0.
* This function provides the best IP address from the list given by getNetworkInterfaces
*/
std::string getBestPublicIp();
/** Converts file size to human readable format.
*
* This function will convert a number to its equivalent size using units.
*
* @param number file size in bytes.
* @return a human-readable string representation of the size, e.g., "2.3 KB", "1.8 MB", "5.2 GB".
*/
std::string beautifyFileSize(uint64_t number);
/**
* Load languages stored in an OPDS stream.
*
* @param content the OPDS stream.
* @return vector containing pairs of language code and their corresponding full language name.
*/
FeedLanguages readLanguagesFromFeed(const std::string& content);
/**
* Load categories stored in an OPDS stream .
*
* @param content the OPDS stream.
* @return vector containing category strings.
*/
FeedCategories readCategoriesFromFeed(const std::string& content);
/**
* Retrieve the full language name associated with a given ISO 639-3 language code.
*
* @param lang ISO 639-3 language code.
* @return full language name.
*/
std::string getLanguageSelfName(const std::string& lang);
/**
* Slugifies the filename by converting any characters reserved by the operating
* system to '_'. Note filename is only the file name and not a path.
*
* @param filename Valid UTF-8 encoded file name string.
* @return slugified string.
*/
std::string getSlugifiedFileName(const std::string& filename);
}
#endif // KIWIX_TOOLS_H

10
kiwix.pc.in Normal file
View File

@@ -0,0 +1,10 @@
prefix=@prefix@
libdir=${prefix}/lib64
includedir=${prefix}/include
Name: libkiwix
Description: A library that contains a lot of things used by used by other kiwix programs
Version: @version@
Requires: @requires@
Libs: -L${libdir} -lkiwix @extra_libs@
Cflags: -I${includedir}/ @extra_cflags@

View File

@@ -1,61 +1,26 @@
project('libkiwix', 'cpp',
version : '14.1.1',
version : '12.1.1',
license : 'GPLv3+',
default_options : ['c_std=c11', 'cpp_std=c++17', 'werror=true'])
default_options : ['c_std=c11', 'cpp_std=c++11', 'werror=true'])
compiler = meson.get_compiler('cpp')
static_deps = get_option('static-linkage') or get_option('default_library') == 'static'
extra_libs = []
# Atomics as compiled by GCC or clang can lead to external references to
# functions depending on the type size and the platform. LLVM provides them in
# 'libcompiler_rt', which clang normally automatically links in, while GNU
# provides them in 'libatomic', which GCC *does not* link in automatically (but
# this is probably going to change, see
# https://gcc.gnu.org/bugzilla/show_bug.cgi?id=81358). Regardless of the setup
# of the compiler driver itself (GCC or clang), we can thus assume that if some
# atomic references can't be resolved, then 'libatomic' is missing.
atomics_program = '''
#include <atomic>
#include <cstdint>
using namespace std;
int main() {
volatile atomic_bool a_b(true);
volatile atomic_ullong a_ull(-1);
// Next two lines are to cover atomic<socket_t> from 'httplib.h'.
volatile atomic<uint32_t> a_u32(-1);
volatile atomic<uint64_t> a_u64(-1);
return atomic_load(&a_b) == false && atomic_load(&a_ull) == 0 &&
atomic_load(&a_u32) == 0 && atomic_load(&a_u64) == 0;
}
'''
if not compiler.links(atomics_program,
name: 'compiler driver readily supports atomics')
libatomic = compiler.find_library('atomic')
compiler.links(atomics_program, name: 'atomics work with libatomic',
dependencies: libatomic, required: true)
extra_libs += ['-latomic']
# See https://github.com/kiwix/libkiwix/issues/371
if ['arm', 'mips', 'm68k', 'ppc', 'sh4'].contains(host_machine.cpu_family())
extra_libs = ['-latomic']
else
extra_libs = []
endif
# C++ std::thread is implemented using pthread on Linux by GCC, and on FreeBSD
# for both GCC and LLVM.
if (host_machine.system() == 'linux' and compiler.get_id() == 'gcc') or \
host_machine.system() == 'freebsd'
if (compiler.get_id() == 'gcc' and build_machine.system() == 'linux') or host_machine.system() == 'freebsd'
# C++ std::thread is implemented using pthread on linux by gcc
thread_dep = dependency('threads')
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,10 +35,9 @@ 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 : '>=8.1.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.')
error('Libzim seems to be compiled without xapian. Xapian support is mandatory.')
endif
@@ -85,25 +49,21 @@ endif
if host_machine.system() == 'windows'
add_project_arguments('-DNOMINMAX', language: 'cpp')
extra_libs += ['-liphlpapi']
endif
if build_machine.system() == 'windows'
extra_libs += ['-lshlwapi', '-lwinmm']
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)
conf = configuration_data()
conf.set('LIBKIWIX_VERSION', '"@0@"'.format(meson.project_version()))
if build_machine.system() == 'windows'
extra_link_args = ['-lshlwapi', '-lwinmm']
else
extra_link_args = []
endif
subdir('include')
subdir('scripts')
subdir('static')
@@ -113,10 +73,17 @@ if get_option('doc')
subdir('docs')
endif
pkg_mod = import('pkgconfig')
pkg_mod.generate(libraries : [libkiwix] + extra_libs,
version : meson.project_version(),
name : 'libkiwix',
filebase : 'libkiwix',
description : 'A library that contains useful primitives that Kiwix readers have in common',
extra_cflags: extra_cflags)
pkg_requires = ['libzim', 'icu-i18n', 'pugixml', 'libcurl', 'libmicrohttpd', 'xapian-core']
pkg_conf = configuration_data()
pkg_conf.set('prefix', get_option('prefix'))
pkg_conf.set('requires', ' '.join(pkg_requires))
pkg_conf.set('extra_libs', ' '.join(extra_libs))
pkg_conf.set('extra_cflags', extra_cflags)
pkg_conf.set('version', meson.project_version())
configure_file(output : 'kiwix.pc',
configuration : pkg_conf,
input : 'kiwix.pc.in',
install_dir: get_option('libdir')+'/pkgconfig'
)

View File

@@ -61,7 +61,7 @@ lang_table_entry_cxx_template = '''
cxxfile_template = '''// This file is automatically generated. Do not modify it.
#include "server/i18n_utils.h"
#include "server/i18n.h"
namespace kiwix {
namespace i18n {

View File

@@ -61,32 +61,6 @@ resource_decl_template = """{namespaces_open}
extern const std::string {identifier};
{namespaces_close}"""
BINARY_RESOURCE_EXTENSIONS = {'.ico', '.png', '.ttf'}
TEXT_RESOURCE_EXTENSIONS = {
'.css',
'.html',
'.js',
'.json',
'.svg',
'.tmpl',
'.webmanifest',
'.xml',
}
if not BINARY_RESOURCE_EXTENSIONS.isdisjoint(TEXT_RESOURCE_EXTENSIONS):
raise RuntimeError(f"The following file type extensions are declared to be both binary and text: {BINARY_RESOURCE_EXTENSIONS.intersection(TEXT_RESOURCE_EXTENSIONS)}")
def is_binary_resource(filename):
_, extension = os.path.splitext(filename)
is_binary = extension in BINARY_RESOURCE_EXTENSIONS
is_text = extension in TEXT_RESOURCE_EXTENSIONS
if not is_binary and not is_text:
# all file type extensions of static resources must be listed
# in either BINARY_RESOURCE_EXTENSIONS or TEXT_RESOURCE_EXTENSIONS
raise RuntimeError(f"Unknown file type extension: {extension}")
return is_binary
class Resource:
def __init__(self, base_dirs, filename, cacheid=None):
filename = filename
@@ -98,8 +72,6 @@ class Resource:
try:
with open(os.path.join(base_dir, filename), 'rb') as f:
self.data = f.read()
if not is_binary_resource(filename):
self.data = self.data.replace(b"\r\n", b"\n")
found = True
break
except FileNotFoundError:

View File

@@ -4,7 +4,6 @@
#include "xmlrpc.h"
#include <iostream>
#include <algorithm>
#include <fstream>
#include <sstream>
#include <thread>
#include <chrono>
@@ -30,41 +29,18 @@
namespace kiwix {
namespace {
void pauseAnyActiveDownloads(const std::string& ariaSessionFilePath)
{
std::ifstream inputFile(ariaSessionFilePath);
if ( !inputFile )
return;
std::ostringstream ss;
std::string line;
while ( std::getline(inputFile, line) ) {
if ( !startsWith(line, " pause=") ) {
ss << line << "\n";
}
if ( !line.empty() && line[0] != ' ' && line[0] != '#' ) {
ss << " pause=true\n";
}
}
std::ofstream outputFile(ariaSessionFilePath);
outputFile << ss.str();
}
} // unnamed namespace
Aria2::Aria2(std::string sessionFileDir):
Aria2::Aria2():
mp_aria(nullptr),
m_port(42042),
m_secret(getNewRpcSecret())
{
m_downloadDir = getDataDirectory();
makeDirectory(m_downloadDir);
std::vector<const char*> callCmd;
std::string rpc_port = "--rpc-listen-port=" + to_string(m_port);
std::string session_file = appendToDirectory(sessionFileDir, "kiwix.session");
pauseAnyActiveDownloads(session_file);
std::string download_dir = "--dir=" + getDataDirectory();
std::string session_file = appendToDirectory(getDataDirectory(), "kiwix.session");
std::string session = "--save-session=" + session_file;
std::string inputFile = "--input-file=" + session_file;
// std::string log_dir = "--log=\"" + logDir + "\"";
@@ -91,6 +67,7 @@ Aria2::Aria2(std::string sessionFileDir):
callCmd.push_back("--enable-rpc");
callCmd.push_back(rpc_secret.c_str());
callCmd.push_back(rpc_port.c_str());
callCmd.push_back(download_dir.c_str());
if (fileReadable(session_file)) {
callCmd.push_back(inputFile.c_str());
}
@@ -120,30 +97,20 @@ Aria2::Aria2(std::string sessionFileDir):
curl_easy_setopt(p_curl, CURLOPT_PORT, m_port);
curl_easy_setopt(p_curl, CURLOPT_POST, 1L);
curl_easy_setopt(p_curl, CURLOPT_ERRORBUFFER, curlErrorBuffer);
curl_easy_setopt(p_curl, CURLOPT_TIMEOUT_MS, 100);
typedef std::chrono::duration<double> Seconds;
const double MAX_WAITING_TIME_SECONDS = 1;
const auto t0 = std::chrono::steady_clock::now();
bool maxWaitingTimeWasExceeded = false;
CURLcode res = CURLE_OK;
while ( !maxWaitingTimeWasExceeded ) {
int watchdog = 50;
while(--watchdog) {
sleep(10);
curlErrorBuffer[0] = 0;
res = curl_easy_perform(p_curl);
auto res = curl_easy_perform(p_curl);
if (res == CURLE_OK) {
break;
} else if (watchdog == 1) {
LOG_ARIA_ERROR();
}
const auto dt = std::chrono::steady_clock::now() - t0;
const double elapsedTime = std::chrono::duration_cast<Seconds>(dt).count();
maxWaitingTimeWasExceeded = elapsedTime > MAX_WAITING_TIME_SECONDS;
}
curl_easy_cleanup(p_curl);
if ( maxWaitingTimeWasExceeded ) {
LOG_ARIA_ERROR();
if (!watchdog) {
throw std::runtime_error("Cannot connect to aria2c rpc. Aria2c launch cmd : " + launchCmd);
}
}

View File

@@ -22,10 +22,11 @@ class Aria2
std::unique_ptr<Subprocess> mp_aria;
int m_port;
std::string m_secret;
std::string m_downloadDir;
std::string doRequest(const MethodCall& methodCall);
public:
explicit Aria2(std::string sessionFileDir);
Aria2();
virtual ~Aria2() = default;
void close();

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

@@ -18,7 +18,6 @@
*/
#include "bookmark.h"
#include "book.h"
#include <pugixml.hpp>
@@ -29,17 +28,6 @@ Bookmark::Bookmark()
{
}
Bookmark::Bookmark(const Book& book, const std::string& path, const std::string& title):
m_bookId(book.getId()),
m_bookTitle(book.getTitle()),
m_bookName(book.getName()),
m_bookFlavour(book.getFlavour()),
m_url(path),
m_title(title),
m_language(book.getCommaSeparatedLanguages()),
m_date(book.getDate())
{}
/* Destructor */
Bookmark::~Bookmark()
{
@@ -50,8 +38,6 @@ void Bookmark::updateFromXml(const pugi::xml_node& node)
auto bookNode = node.child("book");
m_bookId = bookNode.child("id").child_value();
m_bookTitle = bookNode.child("title").child_value();
m_bookName = bookNode.child("name").child_value();
m_bookFlavour = bookNode.child("flavour").child_value();
m_language = bookNode.child("language").child_value();
m_date = bookNode.child("date").child_value();
m_title = node.child("title").child_value();

View File

@@ -18,7 +18,6 @@
*/
#include "downloader.h"
#include "tools.h"
#include "tools/pathTools.h"
#include "tools/stringTools.h"
@@ -125,8 +124,8 @@ void Download::cancelDownload()
}
/* Constructor */
Downloader::Downloader(std::string sessionFileDir) :
mp_aria(new Aria2(sessionFileDir))
Downloader::Downloader() :
mp_aria(new Aria2())
{
try {
for (auto gid : mp_aria->tellWaiting()) {
@@ -151,23 +150,14 @@ Downloader::Downloader(std::string sessionFileDir) :
/* Destructor */
Downloader::~Downloader()
{
close();
}
void Downloader::close()
{
if ( mp_aria ) {
try {
mp_aria->close();
} catch (const std::exception& err) {
std::cerr << "ERROR: Failed to save the downloader state: "
<< err.what() << std::endl;
}
mp_aria.reset();
}
mp_aria->close();
}
std::vector<std::string> Downloader::getDownloadIds() const {
std::vector<std::string> Downloader::getDownloadIds() {
std::unique_lock<std::mutex> lock(m_lock);
std::vector<std::string> ret;
for(auto& p:m_knownDownloads) {
@@ -176,73 +166,37 @@ std::vector<std::string> Downloader::getDownloadIds() const {
return ret;
}
namespace
{
bool downloadCanBeReused(const Download& d,
const std::string& uri,
const Downloader::Options& /*options*/)
{
const auto& uris = d.getUris();
const bool sameURI = std::find(uris.begin(), uris.end(), uri) != uris.end();
if ( !sameURI )
return false;
switch ( d.getStatus() ) {
case Download::K_ERROR:
case Download::K_UNKNOWN:
case Download::K_REMOVED:
return false;
case Download::K_ACTIVE:
case Download::K_WAITING:
case Download::K_PAUSED:
return true; // XXX: what if options are different?
case Download::K_COMPLETE:
return fileExists(d.getPath()); // XXX: what if options are different?
}
return false;
}
} // unnamed namespace
std::shared_ptr<Download> Downloader::startDownload(const std::string& uri, const std::string& downloadDir, Options options)
Download* Downloader::startDownload(const std::string& uri, const std::vector<std::pair<std::string, std::string>>& options)
{
std::unique_lock<std::mutex> lock(m_lock);
options.erase(std::remove_if(options.begin(), options.end(), [](const auto& option) {
return option.first == "dir";
}), options.end());
options.push_back({"dir", downloadDir});
for (auto& p: m_knownDownloads) {
auto& d = p.second;
if ( downloadCanBeReused(*d, uri, options) )
return d;
auto& uris = d->getUris();
if (std::find(uris.begin(), uris.end(), uri) != uris.end())
return d.get();
}
std::vector<std::string> uris = {uri};
auto gid = mp_aria->addUri(uris, options);
m_knownDownloads[gid] = std::make_shared<Download>(mp_aria, gid);
return m_knownDownloads[gid];
m_knownDownloads[gid] = std::unique_ptr<Download>(new Download(mp_aria, gid));
return m_knownDownloads[gid].get();
}
std::shared_ptr<Download> Downloader::getDownload(const std::string& did)
Download* Downloader::getDownload(const std::string& did)
{
std::unique_lock<std::mutex> lock(m_lock);
try {
return m_knownDownloads.at(did);
return m_knownDownloads.at(did).get();
} catch(std::exception& e) {
for (auto gid : mp_aria->tellWaiting()) {
if (gid == did) {
m_knownDownloads[gid] = std::make_shared<Download>(mp_aria, gid);
return m_knownDownloads[gid];
m_knownDownloads[gid] = std::unique_ptr<Download>(new Download(mp_aria, gid));
return m_knownDownloads[gid].get();
}
}
for (auto gid : mp_aria->tellActive()) {
if (gid == did) {
m_knownDownloads[gid] = std::make_shared<Download>(mp_aria, gid);
return m_knownDownloads[gid];
m_knownDownloads[gid] = std::unique_ptr<Download>(new Download(mp_aria, gid));
return m_knownDownloads[gid].get();
}
}
throw e;

View File

@@ -3,7 +3,7 @@
#include "tools/otherTools.h"
#include "tools.h"
#include "tools/regexTools.h"
#include "server/i18n_utils.h"
#include "server/i18n.h"
namespace kiwix
{
@@ -27,30 +27,6 @@ std::string humanFriendlyTitle(std::string title)
return humanFriendlyString;
}
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;
langShortString = "mul";
for (const auto& lang : bookLanguages) {
const std::string fullLang = getLanguageSelfName(lang);
mulLanguages.push_back(fullLang);
}
langFullString = kiwix::join(mulLanguages, ",");
} else if(bookLanguages.size() == 1) {
langShortString = bookLanguages[0];
langFullString = getLanguageSelfName(langShortString);
}
kainjow::mustache::object langTag;
langTag["langShortString"] = langShortString;
langTag["langFullString"] = langFullString;
return langTag;
}
kainjow::mustache::list getTagList(std::string tags)
{
const auto tagsList = kiwix::split(tags, ";", true, false);
@@ -96,16 +72,17 @@ std::string HTMLDumper::dumpPlainHTML(kiwix::Filter filter) const
contentId = urlEncode(nameMapper->getNameForId(bookId));
} catch (...) {}
const auto bookDescription = bookObj.getDescription();
const auto langCode = bookObj.getCommaSeparatedLanguages();
const auto bookIconUrl = rootLocation + "/catalog/v2/illustration/" + bookId + "/?size=48";
const auto tags = bookObj.getTags();
const auto downloadAvailable = (bookObj.getUrl() != "");
const auto langTagObj = getLangTag(bookObj.getLanguages());
std::string faviconAttr = "style=background-image:url(" + bookIconUrl + ")";
booksData.push_back(kainjow::mustache::object{
{"id", contentId},
{"title", bookTitle},
{"description", bookDescription},
{"langTag", langTagObj},
{"langCode", langCode},
{"faviconAttr", faviconAttr},
{"tagList", getTagList(tags)},
{"downloadAvailable", downloadAvailable}
@@ -130,7 +107,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

@@ -39,8 +39,6 @@
namespace kiwix
{
namespace
{
@@ -60,8 +58,6 @@ bool booksReferToTheSameArchive(const Book& book1, const Book& book2)
&& book1.getPath() == book2.getPath();
}
} // unnamed namespace
template<typename Key, typename Value>
class MultiKeyCache: public ConcurrentCache<std::set<Key>, Value>
{
@@ -83,8 +79,49 @@ class MultiKeyCache: public ConcurrentCache<std::set<Key>, Value>
}
};
} // unnamed namespace
struct Library::Impl
{
struct Entry : Book
{
Library::Revision lastUpdatedRevision = 0;
};
Library::Revision m_revision;
std::map<std::string, Entry> m_books;
using ArchiveCache = ConcurrentCache<std::string, std::shared_ptr<zim::Archive>>;
std::unique_ptr<ArchiveCache> mp_archiveCache;
using SearcherCache = MultiKeyCache<std::string, std::shared_ptr<ZimSearcher>>;
std::unique_ptr<SearcherCache> mp_searcherCache;
std::vector<kiwix::Bookmark> m_bookmarks;
Xapian::WritableDatabase m_bookDB;
unsigned int getBookCount(const bool localBooks, const bool remoteBooks) const;
Impl();
~Impl();
Impl(Impl&& );
Impl& operator=(Impl&& );
};
Library::Impl::Impl()
: mp_archiveCache(new ArchiveCache(std::max(getEnvVar<int>("KIWIX_ARCHIVE_CACHE_SIZE", 1), 1))),
mp_searcherCache(new SearcherCache(std::max(getEnvVar<int>("KIWIX_SEARCHER_CACHE_SIZE", 1), 1))),
m_bookDB("", Xapian::DB_BACKEND_INMEMORY)
{
}
Library::Impl::~Impl()
{
}
Library::Impl::Impl(Library::Impl&& ) = default;
Library::Impl& Library::Impl::operator=(Library::Impl&& ) = default;
unsigned int
Library::getBookCount_not_protected(const bool localBooks, const bool remoteBooks) const
Library::Impl::getBookCount(const bool localBooks, const bool remoteBooks) const
{
unsigned int result = 0;
for (auto& pair: m_books) {
@@ -99,41 +136,50 @@ Library::getBookCount_not_protected(const bool localBooks, const bool remoteBook
/* Constructor */
Library::Library()
: mp_archiveCache(new ArchiveCache(std::max(getEnvVar<int>("KIWIX_ARCHIVE_CACHE_SIZE", 1), 1))),
mp_searcherCache(new SearcherCache(std::max(getEnvVar<int>("KIWIX_SEARCHER_CACHE_SIZE", 1), 1))),
m_bookDB(new Xapian::WritableDatabase("", Xapian::DB_BACKEND_INMEMORY))
: mp_impl(new Library::Impl)
{
}
Library::Library(Library&& other)
: mp_impl(std::move(other.mp_impl))
{
}
Library& Library::operator=(Library&& other)
{
mp_impl = std::move(other.mp_impl);
return *this;
}
/* Destructor */
Library::~Library() = default;
bool Library::addBook(const Book& book)
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
++m_revision;
std::lock_guard<std::mutex> lock(m_mutex);
++mp_impl->m_revision;
/* Try to find it */
updateBookDB(book);
try {
auto& oldbook = m_books.at(book.getId());
auto& oldbook = mp_impl->m_books.at(book.getId());
if ( ! booksReferToTheSameArchive(oldbook, book) ) {
dropCache(book.getId());
}
oldbook.update(book); // XXX: This may have no effect if oldbook is readonly
// XXX: Then m_bookDB will become out-of-sync with
// XXX: the real contents of the library.
oldbook.lastUpdatedRevision = m_revision;
oldbook.lastUpdatedRevision = mp_impl->m_revision;
return false;
} catch (std::out_of_range&) {
auto& newEntry = m_books[book.getId()];
auto& newEntry = mp_impl->m_books[book.getId()];
static_cast<Book&>(newEntry) = book;
newEntry.lastUpdatedRevision = m_revision;
size_t new_cache_size = static_cast<size_t>(std::ceil(getBookCount_not_protected(true, true)*0.1));
newEntry.lastUpdatedRevision = mp_impl->m_revision;
size_t new_cache_size = static_cast<size_t>(std::ceil(mp_impl->getBookCount(true, true)*0.1));
if (getEnvVar<int>("KIWIX_ARCHIVE_CACHE_SIZE", -1) <= 0) {
mp_archiveCache->setMaxSize(new_cache_size);
mp_impl->mp_archiveCache->setMaxSize(new_cache_size);
}
if (getEnvVar<int>("KIWIX_SEARCHER_CACHE_SIZE", -1) <= 0) {
mp_searcherCache->setMaxSize(new_cache_size);
mp_impl->mp_searcherCache->setMaxSize(new_cache_size);
}
return true;
}
@@ -141,186 +187,33 @@ bool Library::addBook(const Book& book)
void Library::addBookmark(const Bookmark& bookmark)
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
m_bookmarks.push_back(bookmark);
std::lock_guard<std::mutex> lock(m_mutex);
mp_impl->m_bookmarks.push_back(bookmark);
}
bool Library::removeBookmark(const std::string& zimId, const std::string& url)
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
for(auto it=m_bookmarks.begin(); it!=m_bookmarks.end(); it++) {
std::lock_guard<std::mutex> lock(m_mutex);
for(auto it=mp_impl->m_bookmarks.begin(); it!=mp_impl->m_bookmarks.end(); it++) {
if (it->getBookId() == zimId && it->getUrl() == url) {
m_bookmarks.erase(it);
mp_impl->m_bookmarks.erase(it);
return true;
}
}
return false;
}
std::tuple<int, int> Library::migrateBookmarks(MigrationMode migrationMode) {
std::set<std::string> sourceBooks;
int invalidBookmarks = 0;
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
for(auto& bookmark:m_bookmarks) {
if (m_books.find(bookmark.getBookId()) == m_books.end()) {
invalidBookmarks += 1;
sourceBooks.insert(bookmark.getBookId());
}
}
}
int changed = 0;
for(auto& sourceBook:sourceBooks) {
changed += migrateBookmarks(sourceBook, migrationMode);
}
return std::make_tuple(changed, invalidBookmarks);
}
std::string Library::getBestFromBookCollection(BookIdCollection books, const Bookmark& bookmark, MigrationMode migrationMode) const {
// This function try to get the best book for a bookmark from a book collection.
// It assumes that all books in the collection are "acceptable".
// (this definiton is not clear but for now it is book's name is equal to bookmark's bookName)
//
// The algorithm first sort the colletion by "flavour equality" and date.
// "flavour equality" is if book's flavour is same that bookmark's flavour (let's say "flavourA" here)
// So we have the sorted collection:
// - flavourA, date 5
// - flavourA, date 4
// - flavourB, date 6
// - flavourC, date 5
// - flavourB, date 3
//
// Then, depending of migrationMode:
// - If ALLOW_DOWNGRADE => take the first one
// - If UPGRADE_ONLY => loop on books until we find a book newer than bookmark.
// So if bookmark date is 5 => flavourB, date 6
// if bookmark date is 4 => flavourA, date 5
// if bookmark date is 7 => No book
if (books.empty()) {
return "";
}
sort(books, DATE, false);
stable_sort(books.begin(), books.end(), [&](const std::string& bookId1, const std::string& bookId2) {
const auto& book1 = getBookById(bookId1);
const auto& book2 = getBookById(bookId2);
bool same_flavour1 = book1.getFlavour() == bookmark.getBookFlavour();
bool same_flavour2 = book2.getFlavour() == bookmark.getBookFlavour();
// return True if bookId1 is before bookId2, ie if same_flavour1 and not same_flavour2
return same_flavour1 > same_flavour2;
});
if (migrationMode == ALLOW_DOWNGRADE) {
return books[0];
} else {
for (const auto& bookId: books) {
const auto& book = getBookById(bookId);
if (book.getDate() >= bookmark.getDate()) {
return bookId;
}
}
}
return "";
}
std::string remove_quote(std::string input) {
std::replace(input.begin(), input.end(), '"', ' ');
return input;
}
std::string Library::getBestTargetBookId(const std::string& bookName, const std::string& preferedFlavour, const std::string& minDate) const {
// Let's reuse our algorithm based on bookmark.
MigrationMode migrationMode = UPGRADE_ONLY;
auto bookmark = Bookmark();
bookmark.setBookName(bookName);
bookmark.setBookFlavour(preferedFlavour);
if (minDate.empty()) {
migrationMode = ALLOW_DOWNGRADE;
} else {
bookmark.setDate(minDate);
}
return getBestTargetBookId(bookmark, migrationMode);
}
std::string Library::getBestTargetBookId(const Bookmark& bookmark, MigrationMode migrationMode) const {
std::lock_guard<std::recursive_mutex> lock(m_mutex);
// Search for a existing book with the same name
auto book_filter = Filter();
if (!bookmark.getBookName().empty()) {
book_filter.name(bookmark.getBookName());
} else {
// We don't have a name stored (older bookmarks)
// Fallback on title (All bookmarks should have one, but let's be safe against wrongly filled bookmark)
if (bookmark.getBookTitle().empty()) {
// No bookName nor bookTitle, no way to find target book.
return "";
}
book_filter.query("title:\"" + remove_quote(bookmark.getBookTitle()) + "\"");
}
auto targetBooks = filter(book_filter);
auto bestBook = getBestFromBookCollection(targetBooks, bookmark, migrationMode);
if (bestBook.empty()) {
try {
getBookById(bookmark.getBookId());
return bookmark.getBookId();
} catch (std::out_of_range&) {}
}
return bestBook;
}
int Library::migrateBookmarks(const std::string& sourceBookId, MigrationMode migrationMode) {
std::lock_guard<std::recursive_mutex> lock(m_mutex);
Bookmark firstBookmarkToChange;
for(auto& bookmark:m_bookmarks) {
if (bookmark.getBookId() == sourceBookId) {
firstBookmarkToChange = bookmark;
break;
}
}
if (firstBookmarkToChange.getBookId().empty()) {
return 0;
}
std::string betterBook = getBestTargetBookId(firstBookmarkToChange, migrationMode);
if (betterBook.empty()) {
return 0;
}
return migrateBookmarks(sourceBookId, betterBook);
}
int Library::migrateBookmarks(const std::string& sourceBookId, const std::string& targetBookId) {
if (sourceBookId == targetBookId) {
return 0;
}
int changed = 0;
for (auto& bookmark:m_bookmarks) {
if (bookmark.getBookId() == sourceBookId) {
bookmark.setBookId(targetBookId);
changed +=1;
}
}
return changed;
}
void Library::dropCache(const std::string& id)
{
mp_archiveCache->drop(id);
mp_searcherCache->drop(id);
mp_impl->mp_archiveCache->drop(id);
mp_impl->mp_searcherCache->drop(id);
}
bool Library::removeBookById(const std::string& id)
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
m_bookDB->delete_document("Q" + id);
std::lock_guard<std::mutex> lock(m_mutex);
mp_impl->m_bookDB.delete_document("Q" + id);
dropCache(id);
// We do not change the cache size here
// Most of the time, the book is remove in case of library refresh, it is
@@ -328,25 +221,25 @@ bool Library::removeBookById(const std::string& id)
// Having a too big cache is not a problem here (or it would have been before)
// (And setMaxSize doesn't actually reduce the cache size, extra cached items
// will be removed in put or getOrPut).
const bool bookWasRemoved = m_books.erase(id) == 1;
const bool bookWasRemoved = mp_impl->m_books.erase(id) == 1;
if ( bookWasRemoved ) {
++m_revision;
++mp_impl->m_revision;
}
return bookWasRemoved;
}
Library::Revision Library::getRevision() const
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
return m_revision;
std::lock_guard<std::mutex> lock(m_mutex);
return mp_impl->m_revision;
}
uint32_t Library::removeBooksNotUpdatedSince(Revision libraryRevision)
{
BookIdCollection booksToRemove;
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
for ( const auto& entry : m_books) {
std::lock_guard<std::mutex> lock(m_mutex);
for ( const auto& entry : mp_impl->m_books) {
if ( entry.second.lastUpdatedRevision <= libraryRevision ) {
booksToRemove.push_back(entry.first);
}
@@ -365,12 +258,12 @@ const Book& Library::getBookById(const std::string& id) const
{
// XXX: Doesn't make sense to lock this operation since it cannot
// XXX: guarantee thread-safety because of its return type
return m_books.at(id);
return mp_impl->m_books.at(id);
}
Book Library::getBookByIdThreadSafe(const std::string& id) const
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
std::lock_guard<std::mutex> lock(m_mutex);
return getBookById(id);
}
@@ -378,7 +271,7 @@ const Book& Library::getBookByPath(const std::string& path) const
{
// XXX: Doesn't make sense to lock this operation since it cannot
// XXX: guarantee thread-safety because of its return type
for(auto& it: m_books) {
for(auto& it: mp_impl->m_books) {
auto& book = it.second;
if (book.getPath() == path)
return book;
@@ -391,7 +284,7 @@ const Book& Library::getBookByPath(const std::string& path) const
std::shared_ptr<zim::Archive> Library::getArchiveById(const std::string& id)
{
try {
return mp_archiveCache->getOrPut(id,
return mp_impl->mp_archiveCache->getOrPut(id,
[&](){
auto book = getBookById(id);
if (!book.isPathValid()) {
@@ -408,7 +301,7 @@ std::shared_ptr<ZimSearcher> Library::getSearcherByIds(const BookIdSet& ids)
{
assert(!ids.empty());
try {
return mp_searcherCache->getOrPut(ids,
return mp_impl->mp_searcherCache->getOrPut(ids,
[&](){
std::vector<zim::Archive> archives;
for(auto& id:ids) {
@@ -428,8 +321,8 @@ std::shared_ptr<ZimSearcher> Library::getSearcherByIds(const BookIdSet& ids)
unsigned int Library::getBookCount(const bool localBooks,
const bool remoteBooks) const
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
return getBookCount_not_protected(localBooks, remoteBooks);
std::lock_guard<std::mutex> lock(m_mutex);
return mp_impl->getBookCount(localBooks, remoteBooks);
}
bool Library::writeToFile(const std::string& path) const
@@ -441,7 +334,7 @@ bool Library::writeToFile(const std::string& path) const
dumper.setBaseDir(baseDir);
std::string xml;
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
std::lock_guard<std::mutex> lock(m_mutex);
xml = dumper.dumpLibXMLContent(allBookIds);
};
return writeTextFile(path, xml);
@@ -457,10 +350,10 @@ bool Library::writeBookmarksToFile(const std::string& path) const
Library::AttributeCounts Library::getBookAttributeCounts(BookStrPropMemFn p) const
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
std::lock_guard<std::mutex> lock(m_mutex);
AttributeCounts propValueCounts;
for (const auto& pair: m_books) {
for (const auto& pair: mp_impl->m_books) {
const auto& book = pair.second;
if (book.getOrigId().empty()) {
propValueCounts[(book.*p)()] += 1;
@@ -489,10 +382,10 @@ std::vector<std::string> Library::getBooksLanguages() const
Library::AttributeCounts Library::getBooksLanguagesWithCounts() const
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
std::lock_guard<std::mutex> lock(m_mutex);
AttributeCounts langsWithCounts;
for (const auto& pair: m_books) {
for (const auto& pair: mp_impl->m_books) {
const auto& book = pair.second;
if (book.getOrigId().empty()) {
for ( const auto& lang : book.getLanguages() ) {
@@ -505,10 +398,10 @@ Library::AttributeCounts Library::getBooksLanguagesWithCounts() const
std::vector<std::string> Library::getBooksCategories() const
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
std::lock_guard<std::mutex> lock(m_mutex);
std::set<std::string> categories;
for (const auto& pair: m_books) {
for (const auto& pair: mp_impl->m_books) {
const auto& book = pair.second;
const auto& c = book.getCategory();
if ( !c.empty() ) {
@@ -532,12 +425,12 @@ std::vector<std::string> Library::getBooksPublishers() const
const std::vector<kiwix::Bookmark> Library::getBookmarks(bool onlyValidBookmarks) const
{
if (!onlyValidBookmarks) {
return m_bookmarks;
return mp_impl->m_bookmarks;
}
std::vector<kiwix::Bookmark> validBookmarks;
auto booksId = getBooksIds();
std::lock_guard<std::recursive_mutex> lock(m_mutex);
for(auto& bookmark:m_bookmarks) {
std::lock_guard<std::mutex> lock(m_mutex);
for(auto& bookmark:mp_impl->m_bookmarks) {
if (std::find(booksId.begin(), booksId.end(), bookmark.getBookId()) != booksId.end()) {
validBookmarks.push_back(bookmark);
}
@@ -547,10 +440,10 @@ const std::vector<kiwix::Bookmark> Library::getBookmarks(bool onlyValidBookmarks
Library::BookIdCollection Library::getBooksIds() const
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
std::lock_guard<std::mutex> lock(m_mutex);
BookIdCollection bookIds;
for (auto& pair: m_books) {
for (auto& pair: mp_impl->m_books) {
bookIds.push_back(pair.first);
}
@@ -589,8 +482,7 @@ void Library::updateBookDB(const Book& book)
}
indexer.index_text(normalizeText(book.getCreator()), 1, "A");
indexer.index_text(normalizeText(book.getPublisher()), 1, "XP");
doc.add_term("XN"+normalizeText(book.getName()));
indexer.index_text(normalizeText(book.getFlavour()), 1, "XF");
indexer.index_text(normalizeText(book.getName()), 1, "XN");
indexer.index_text(normalizeText(book.getCategory()), 1, "XC");
for ( const auto& tag : split(normalizeText(book.getTags()), ";") ) {
@@ -606,7 +498,7 @@ void Library::updateBookDB(const Book& book)
doc.set_data(book.getId());
m_bookDB->replace_document(idterm, doc);
mp_impl->m_bookDB.replace_document(idterm, doc);
}
namespace
@@ -631,7 +523,6 @@ Xapian::Query buildXapianQueryFromFilterQuery(const Filter& filter)
queryParser.add_prefix("title", "S");
queryParser.add_prefix("description", "XD");
queryParser.add_prefix("name", "XN");
queryParser.add_prefix("flavour", "XF");
queryParser.add_prefix("category", "XC");
queryParser.add_prefix("lang", "L");
queryParser.add_prefix("publisher", "XP");
@@ -645,6 +536,8 @@ Xapian::Query buildXapianQueryFromFilterQuery(const Filter& filter)
//queryParser.set_stemmer(Xapian::Stem(iso639_3ToXapian(???)));
//queryParser.set_stemming_strategy(Xapian::QueryParser::STEM_SOME);
const auto flags = Xapian::QueryParser::FLAG_PHRASE
| Xapian::QueryParser::FLAG_BOOLEAN
| Xapian::QueryParser::FLAG_BOOLEAN_ANY_CASE
| Xapian::QueryParser::FLAG_LOVEHATE
| Xapian::QueryParser::FLAG_WILDCARD
| partialQueryFlag;
@@ -656,35 +549,25 @@ Xapian::Query nameQuery(const std::string& name)
return Xapian::Query("XN" + normalizeText(name));
}
Xapian::Query flavourQuery(const std::string& name)
Xapian::Query categoryQuery(const std::string& category)
{
return Xapian::Query("XF" + normalizeText(name));
}
Xapian::Query multipleParamQuery(const std::string& commaSeparatedList, const std::string& prefix)
{
Xapian::Query q;
bool firstIteration = true;
for ( const auto& elem : kiwix::split(commaSeparatedList, ",") ) {
const Xapian::Query singleQuery(prefix + normalizeText(elem));
if ( firstIteration ) {
q = singleQuery;
firstIteration = false;
} else {
q = Xapian::Query(Xapian::Query::OP_OR, q, singleQuery);
}
}
return q;
}
Xapian::Query categoryQuery(const std::string& commaSeparatedCategoryList)
{
return multipleParamQuery(commaSeparatedCategoryList, "XC");
return Xapian::Query("XC" + normalizeText(category));
}
Xapian::Query langQuery(const std::string& commaSeparatedLanguageList)
{
return multipleParamQuery(commaSeparatedLanguageList, "L");
Xapian::Query q;
bool firstIteration = true;
for ( const auto& lang : kiwix::split(commaSeparatedLanguageList, ",") ) {
const Xapian::Query singleLangQuery("L" + normalizeText(lang));
if ( firstIteration ) {
q = singleLangQuery;
firstIteration = false;
} else {
q = Xapian::Query(Xapian::Query::OP_OR, q, singleLangQuery);
}
}
return q;
}
Xapian::Query publisherQuery(const std::string& publisher)
@@ -728,9 +611,6 @@ Xapian::Query buildXapianQuery(const Filter& filter)
if ( filter.hasName() ) {
q = Xapian::Query(Xapian::Query::OP_AND, q, nameQuery(filter.getName()));
}
if ( filter.hasFlavour() ) {
q = Xapian::Query(Xapian::Query::OP_AND, q, flavourQuery(filter.getFlavour()));
}
if ( filter.hasCategory() ) {
q = Xapian::Query(Xapian::Query::OP_AND, q, categoryQuery(filter.getCategory()));
}
@@ -761,10 +641,10 @@ Library::BookIdCollection Library::filterViaBookDB(const Filter& filter) const
BookIdCollection bookIds;
std::lock_guard<std::recursive_mutex> lock(m_mutex);
Xapian::Enquire enquire(*m_bookDB);
std::lock_guard<std::mutex> lock(m_mutex);
Xapian::Enquire enquire(mp_impl->m_bookDB);
enquire.set_query(query);
const auto results = enquire.get_mset(0, m_books.size());
const auto results = enquire.get_mset(0, mp_impl->m_books.size());
for ( auto it = results.begin(); it != results.end(); ++it ) {
bookIds.push_back(it.get_document().get_data());
}
@@ -776,9 +656,9 @@ Library::BookIdCollection Library::filter(const Filter& filter) const
{
BookIdCollection result;
const auto preliminaryResult = filterViaBookDB(filter);
std::lock_guard<std::recursive_mutex> lock(m_mutex);
std::lock_guard<std::mutex> lock(m_mutex);
for(auto id : preliminaryResult) {
if(filter.accept(m_books.at(id))) {
if(filter.accept(mp_impl->m_books.at(id))) {
result.push_back(id);
}
}
@@ -850,7 +730,7 @@ void Library::sort(BookIdCollection& bookIds, supportedListSortBy sort, bool asc
// NOTE: for the entire duration of the sort. Will need to obtain (under a
// NOTE: lock) the required atributes from the books once, and then the
// NOTE: sorting will run on a copy of data without locking.
std::lock_guard<std::recursive_mutex> lock(m_mutex);
std::lock_guard<std::mutex> lock(m_mutex);
switch(sort) {
case TITLE:
std::sort(bookIds.begin(), bookIds.end(), Comparator<TITLE>(this, ascending));
@@ -896,7 +776,6 @@ enum filterTypes {
QUERY = FLAG(12),
NAME = FLAG(13),
CATEGORY = FLAG(14),
FLAVOUR = FLAG(15),
};
Filter& Filter::local(bool accept)
@@ -998,13 +877,6 @@ Filter& Filter::name(std::string name)
activeFilters |= NAME;
return *this;
}
Filter& Filter::flavour(std::string flavour)
{
_flavour = flavour;
activeFilters |= FLAVOUR;
return *this;
}
Filter& Filter::clearLang()
{
@@ -1050,12 +922,6 @@ bool Filter::hasCreator() const
return ACTIVE(_CREATOR);
}
bool Filter::hasFlavour() const
{
return ACTIVE(FLAVOUR);
}
bool Filter::accept(const Book& book) const
{
auto local = !book.getPath().empty();

View File

@@ -23,6 +23,65 @@ void LibraryDumper::setOpenSearchInfo(int totalResults, int startIndex, int coun
m_count = count;
}
namespace {
std::map<std::string, std::string> iso639_3 = {
{"atj", "atikamekw"},
{"azb", "آذربایجان دیلی"},
{"bcl", "central bikol"},
{"bgs", "tagabawa"},
{"bxr", "буряад хэлэн"},
{"cbk", "chavacano"},
{"cdo", "閩東語"},
{"dag", "Dagbani"},
{"diq", "dimli"},
{"dty", "डोटेली"},
{"eml", "emiliân-rumagnōl"},
{"fbs", "српскохрватски"},
{"guw", "Gungbe"},
{"hbs", "srpskohrvatski"},
{"ido", "ido"},
{"kbp", "kabɩ"},
{"kld", "Gamilaraay"},
{"lbe", "лакку маз"},
{"lbj", "ལ་དྭགས་སྐད་"},
{"map", "Austronesian"},
{"mhr", "марий йылме"},
{"mnw", "ဘာသာမန်"},
{"myn", "mayan"},
{"nah", "nahuatl"},
{"nai", "north American Indian"},
{"nds", "plattdütsch"},
{"nrm", "bhasa narom"},
{"olo", "livvi"},
{"pih", "Pitcairn-Norfolk"},
{"pnb", "Western Panjabi"},
{"rmr", "Caló"},
{"rmy", "romani shib"},
{"roa", "romance languages"},
{"twi", "twi"},
};
std::once_flag fillLanguagesFlag;
void fillLanguagesMap()
{
for (auto icuLangPtr = icu::Locale::getISOLanguages(); *icuLangPtr != NULL; ++icuLangPtr) {
const ICULanguageInfo lang(*icuLangPtr);
iso639_3.insert({lang.iso3Code(), lang.selfName()});
}
}
std::string getLanguageSelfName(const std::string& lang) {
const auto itr = iso639_3.find(lang);
if (itr != iso639_3.end()) {
return itr->second;
}
return lang;
};
} // unnamed namespace
kainjow::mustache::list LibraryDumper::getCategoryData() const
{
const auto now = gen_date_str();
@@ -43,6 +102,7 @@ kainjow::mustache::list LibraryDumper::getLanguageData() const
{
const auto now = gen_date_str();
kainjow::mustache::list languageData;
std::call_once(fillLanguagesFlag, fillLanguagesMap);
for ( const auto& langAndBookCount : library->getBooksLanguagesWithCounts() ) {
const std::string languageCode = langAndBookCount.first;
const int bookCount = langAndBookCount.second;

View File

@@ -97,15 +97,11 @@ void LibXMLDumper::handleBookmark(Bookmark bookmark, pugi::xml_node root_node) {
auto book = library->getBookByIdThreadSafe(bookmark.getBookId());
ADD_TEXT_ENTRY(book_node, "id", book.getId());
ADD_TEXT_ENTRY(book_node, "title", book.getTitle());
ADD_TEXT_ENTRY(book_node, "name", book.getName());
ADD_TEXT_ENTRY(book_node, "flavour", book.getFlavour());
ADD_TEXT_ENTRY(book_node, "language", book.getCommaSeparatedLanguages());
ADD_TEXT_ENTRY(book_node, "date", book.getDate());
} catch (...) {
ADD_TEXT_ENTRY(book_node, "id", bookmark.getBookId());
ADD_TEXT_ENTRY(book_node, "title", bookmark.getBookTitle());
ADD_TEXT_ENTRY(book_node, "name", bookmark.getBookName());
ADD_TEXT_ENTRY(book_node, "flavour", bookmark.getBookFlavour());
ADD_TEXT_ENTRY(book_node, "language", bookmark.getLanguage());
ADD_TEXT_ENTRY(book_node, "date", bookmark.getDate());
}
@@ -139,7 +135,7 @@ std::string LibXMLDumper::dumpLibXMLBookmark()
pugi::xml_node bookmarksNode = doc.append_child("bookmarks");
if (library) {
for (auto& bookmark: library->getBookmarks(false)) {
for (auto& bookmark: library->getBookmarks()) {
handleBookmark(bookmark, bookmarksNode);
}
}

View File

@@ -23,24 +23,26 @@
#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
{
namespace
{
struct NoDelete
{
template<class T> void operator()(T*) {}
};
} // unnamed namespace
////////////////////////////////////////////////////////////////////////////////
// LibraryManipulator
////////////////////////////////////////////////////////////////////////////////
LibraryManipulator::LibraryManipulator(LibraryPtr library)
: library(library)
LibraryManipulator::LibraryManipulator(Library* library)
: library(*library)
{}
LibraryManipulator::~LibraryManipulator()
@@ -48,7 +50,7 @@ LibraryManipulator::~LibraryManipulator()
bool LibraryManipulator::addBookToLibrary(const Book& book)
{
const auto ret = library->addBook(book);
const auto ret = library.addBook(book);
if ( ret ) {
bookWasAddedToLibrary(book);
}
@@ -57,13 +59,13 @@ bool LibraryManipulator::addBookToLibrary(const Book& book)
void LibraryManipulator::addBookmarkToLibrary(const Bookmark& bookmark)
{
library->addBookmark(bookmark);
library.addBookmark(bookmark);
bookmarkWasAddedToLibrary(bookmark);
}
uint32_t LibraryManipulator::removeBooksNotUpdatedSince(Library::Revision rev)
{
const auto n = library->removeBooksNotUpdatedSince(rev);
const auto n = library.removeBooksNotUpdatedSince(rev);
if ( n != 0 ) {
booksWereRemovedFromLibrary();
}
@@ -87,15 +89,15 @@ void LibraryManipulator::booksWereRemovedFromLibrary()
////////////////////////////////////////////////////////////////////////////////
/* Constructor */
Manager::Manager(LibraryManipulator manipulator):
Manager::Manager(LibraryManipulator* manipulator):
writableLibraryPath(""),
manipulator(manipulator)
manipulator(manipulator, NoDelete())
{
}
Manager::Manager(LibraryPtr library) :
Manager::Manager(Library* library) :
writableLibraryPath(""),
manipulator(LibraryManipulator(library))
manipulator(new LibraryManipulator(library))
{
}
@@ -119,7 +121,7 @@ bool Manager::parseXmlDom(const pugi::xml_document& doc,
if (!trustLibrary && !book.getPath().empty()) {
this->readBookFromPath(book.getPath(), &book);
}
manipulator.addBookToLibrary(book);
manipulator->addBookToLibrary(book);
}
return true;
@@ -164,7 +166,7 @@ bool Manager::parseOpdsDom(const pugi::xml_document& doc, const std::string& url
book.updateFromOpds(entryNode, urlHost);
/* Update the book properties with the new importer */
manipulator.addBookToLibrary(book);
manipulator->addBookToLibrary(book);
}
return true;
@@ -239,7 +241,7 @@ std::string Manager::addBookFromPathAndGetId(const std::string& pathToOpen,
|| (!book.getTitle().empty() && !book.getLanguages().empty()
&& !book.getDate().empty())) {
book.setUrl(url);
manipulator.addBookToLibrary(book);
manipulator->addBookToLibrary(book);
return book.getId();
}
}
@@ -259,58 +261,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;
@@ -346,7 +296,7 @@ bool Manager::readBookmarkFile(const std::string& path)
bookmark.updateFromXml(node);
manipulator.addBookmarkToLibrary(bookmark);
manipulator->addBookmarkToLibrary(bookmark);
}
return true;
@@ -354,7 +304,7 @@ bool Manager::readBookmarkFile(const std::string& path)
void Manager::reload(const Paths& paths)
{
const auto libRevision = manipulator.getLibrary()->getRevision();
const auto libRevision = manipulator->getLibrary().getRevision();
for (std::string path : paths) {
if (!path.empty()) {
if ( kiwix::isRelativePath(path) )
@@ -366,7 +316,7 @@ void Manager::reload(const Paths& paths)
}
}
manipulator.removeBooksNotUpdatedSince(libRevision);
manipulator->removeBooksNotUpdatedSince(libRevision);
}
}

View File

@@ -17,8 +17,6 @@ kiwix_sources = [
'tools/regexTools.cpp',
'tools/stringTools.cpp',
'tools/networkTools.cpp',
'tools/opdsParsingTools.cpp',
'tools/languageTools.cpp',
'tools/otherTools.cpp',
'tools/archiveTools.cpp',
'kiwixserve.cpp',
@@ -31,7 +29,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,34 +24,30 @@
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;
mapName(library, bookName, bookId);
m_nameToId[bookName] = bookId;
if (!withAlias)
continue;
auto aliasName = replaceRegex(bookName, "", "_[[:digit:]]{4}-[[:digit:]]{2}$");
if (aliasName != bookName) {
mapName(library, aliasName, bookId);
if (aliasName == bookName) {
continue;
}
if (m_nameToId.find(aliasName) == m_nameToId.end()) {
m_nameToId[aliasName] = bookId;
} else {
auto alreadyPresentPath = library.getBookById(m_nameToId[aliasName]).getPath();
std::cerr << "Path collision: " << alreadyPresentPath
<< " and " << currentBook.getPath()
<< " can't share the same URL path '" << aliasName << "'."
<< " Therefore, only " << alreadyPresentPath
<< " will be served." << std::endl;
}
}
}
void HumanReadableNameMapper::mapName(const Library& library, std::string name, std::string bookId) {
if (m_nameToId.find(name) == m_nameToId.end()) {
m_nameToId[name] = bookId;
} else {
const auto& currentBook = library.getBookById(bookId);
auto alreadyPresentPath = library.getBookById(m_nameToId[name]).getPath();
std::cerr << "Path collision: '" << alreadyPresentPath
<< "' and '" << currentBook.getPath()
<< "' can't share the same URL path '" << name << "'."
<< " Therefore, only '" << alreadyPresentPath
<< "' will be served." << std::endl;
}
}
@@ -67,7 +63,7 @@ std::string HumanReadableNameMapper::getIdForName(const std::string& name) const
// UpdatableNameMapper
////////////////////////////////////////////////////////////////////////////////
UpdatableNameMapper::UpdatableNameMapper(LibraryPtr lib, bool withAlias)
UpdatableNameMapper::UpdatableNameMapper(Library& lib, bool withAlias)
: library(lib)
, withAlias(withAlias)
{
@@ -76,7 +72,7 @@ UpdatableNameMapper::UpdatableNameMapper(LibraryPtr lib, bool withAlias)
void UpdatableNameMapper::update()
{
const auto newNameMapper = new HumanReadableNameMapper(*library, withAlias);
const auto newNameMapper = new HumanReadableNameMapper(library, withAlias);
std::lock_guard<std::mutex> lock(mutex);
nameMapper.reset(newNameMapper);
}

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

@@ -32,46 +32,20 @@
#include "libkiwix-resources.h"
#include "tools/stringTools.h"
#include "server/i18n_utils.h"
namespace kiwix
{
namespace
{
ParameterizedMessage searchResultsPageTitleMsg(const std::string& searchPattern)
{
return ParameterizedMessage("search-results-page-title",
{{"SEARCH_PATTERN", searchPattern}}
);
}
ParameterizedMessage searchResultsPageHeaderMsg(const std::string& searchPattern,
const kainjow::mustache::data& r)
{
if ( r.get("count")->string_value() == "0" ) {
return ParameterizedMessage("empty-search-results-page-header",
{{"SEARCH_PATTERN", searchPattern}}
);
} else {
return ParameterizedMessage("search-results-page-header",
{
{"SEARCH_PATTERN", searchPattern},
{"START", r.get("startLabel")->string_value()},
{"END", r.get("end") ->string_value()},
{"COUNT", r.get("count")->string_value()},
}
);
}
}
} // unnamed namespace
/* Constructor */
SearchRenderer::SearchRenderer(zim::SearchResultSet srs,
SearchRenderer::SearchRenderer(zim::SearchResultSet srs, NameMapper* mapper,
unsigned int start, unsigned int estimatedResultCount)
: SearchRenderer(srs, mapper, nullptr, start, estimatedResultCount)
{}
SearchRenderer::SearchRenderer(zim::SearchResultSet srs, NameMapper* mapper, Library* library,
unsigned int start, unsigned int estimatedResultCount)
: m_srs(srs),
mp_nameMapper(mapper),
mp_library(library),
protocolPrefix("zim://"),
searchProtocolPrefix("search://"),
estimatedResultCount(estimatedResultCount),
@@ -190,7 +164,7 @@ kainjow::mustache::data buildPagination(
return pagination;
}
std::string SearchRenderer::renderTemplate(const std::string& tmpl_str, const NameMapper& nameMapper, const Library* library)
std::string SearchRenderer::renderTemplate(const std::string& tmpl_str)
{
const std::string absPathPrefix = protocolPrefix;
// Build the results list
@@ -198,25 +172,15 @@ std::string SearchRenderer::renderTemplate(const std::string& tmpl_str, const Na
for (auto it = m_srs.begin(); it != m_srs.end(); it++) {
kainjow::mustache::data result;
const std::string zim_id(it.getZimId());
const auto path = nameMapper.getNameForId(zim_id) + "/" + it.getPath();
const auto path = mp_nameMapper->getNameForId(zim_id) + "/" + it.getPath();
result.set("title", it.getTitle());
result.set("absolutePath", absPathPrefix + urlEncode(path));
result.set("snippet", it.getSnippet());
if (library) {
const std::string bookTitle = library->getBookById(zim_id).getTitle();
const ParameterizedMessage bookInfoMsg("search-result-book-info",
{{"BOOK_TITLE", bookTitle}}
);
result.set("bookInfo", bookInfoMsg.getText(userlang)); // for HTML
result.set("bookTitle", bookTitle); // for XML
if (mp_library) {
result.set("bookTitle", mp_library->getBookById(zim_id).getTitle());
}
if (it.getWordCount() >= 0) {
const auto wordCountStr = kiwix::beautifyInteger(it.getWordCount());
const ParameterizedMessage wordCountMsg("word-count",
{{"COUNT", wordCountStr}}
);
result.set("wordCountInfo", wordCountMsg.getText(userlang)); // for HTML
result.set("wordCount", wordCountStr); // for XML
result.set("wordCount", kiwix::beautifyInteger(it.getWordCount()));
}
items.push_back(result);
@@ -224,9 +188,9 @@ std::string SearchRenderer::renderTemplate(const std::string& tmpl_str, const Na
kainjow::mustache::data results;
results.set("items", items);
results.set("count", kiwix::beautifyInteger(estimatedResultCount));
results.set("hasResults", estimatedResultCount != 0);
results.set("start", kiwix::beautifyInteger(resultStart));
results.set("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(
@@ -241,15 +205,12 @@ std::string SearchRenderer::renderTemplate(const std::string& tmpl_str, const Na
searchBookQuery
);
const auto pageHeaderMsg = searchResultsPageHeaderMsg(searchPattern, results);
const kainjow::mustache::object allData{
{"PAGE_TITLE", searchResultsPageTitleMsg(searchPattern).getText(userlang)},
{"PAGE_HEADER", pageHeaderMsg.getText(userlang)},
{"searchProtocolPrefix", searchProtocolPrefix},
{"results", results},
{"pagination", pagination},
{"query", query},
};
kainjow::mustache::data allData;
allData.set("searchProtocolPrefix", searchProtocolPrefix);
allData.set("results", results);
allData.set("pagination", pagination);
allData.set("query", query);
kainjow::mustache::mustache tmpl(tmpl_str);
@@ -261,14 +222,14 @@ std::string SearchRenderer::renderTemplate(const std::string& tmpl_str, const Na
return ss.str();
}
std::string SearchRenderer::getHtml(const NameMapper& mapper, const Library* library)
std::string SearchRenderer::getHtml()
{
return renderTemplate(RESOURCE::templates::search_result_html, mapper, library);
return renderTemplate(RESOURCE::templates::search_result_html);
}
std::string SearchRenderer::getXml(const NameMapper& mapper, const Library* library)
std::string SearchRenderer::getXml()
{
return renderTemplate(RESOURCE::templates::search_result_xml, mapper, library);
return renderTemplate(RESOURCE::templates::search_result_xml);
}

View File

@@ -29,23 +29,7 @@
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) :
Server::Server(Library* library, NameMapper* nameMapper) :
mp_library(library),
mp_nameMapper(nameMapper),
mp_server(nullptr)
@@ -67,18 +51,9 @@ bool Server::start() {
m_withTaskbar,
m_withLibraryButton,
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,53 +66,22 @@ 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;
}
void Server::setAddress(const std::string& addr)
{
m_addr.addr.clear();
m_addr.addr6.clear();
if (addr.empty()) return;
if (addr.find(':') != std::string::npos) { // IPv6
m_addr.addr6 = (addr[0] == '[') ? addr.substr(1, addr.length() - 2) : addr; // Remove brackets if any
} else {
m_addr.addr = addr;
if (m_root[0] != '/') {
m_root = "/" + m_root;
}
if (m_root.back() == '/') {
m_root.erase(m_root.size() - 1);
}
}
int Server::getPort() const
int Server::getPort()
{
return m_port;
return mp_server->getPort();
}
IpAddress Server::getAddress() const
std::string Server::getAddress()
{
return m_addr;
}
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;
return mp_server->getAddress();
}
}

View File

@@ -17,7 +17,7 @@
* MA 02110-1301, USA.
*/
#include "i18n_utils.h"
#include "i18n.h"
#include "tools/otherTools.h"
@@ -112,12 +112,8 @@ std::string expandParameterizedString(const std::string& lang,
const std::string& key,
const Parameters& params)
{
kainjow::mustache::object mustacheParams;
for( const auto& kv : params ) {
mustacheParams[kv.first] = kv.second;
}
const std::string tmpl = getTranslatedString(lang, key);
return render_template(tmpl, mustacheParams);
return render_template(tmpl, params);
}
} // namespace i18n
@@ -193,13 +189,4 @@ std::string selectMostSuitableLanguage(const UserLangPreferences& prefs)
return bestLangSoFar;
}
std::string translateBookCategory(const std::string& lang, const std::string& category)
{
try {
return getTranslatedString(lang, "book-category." + category);
} catch (...) {
return category;
}
}
} // namespace kiwix

View File

@@ -17,10 +17,10 @@
* MA 02110-1301, USA.
*/
#ifndef KIWIX_SERVER_I18N_UTILS
#define KIWIX_SERVER_I18N_UTILS
#ifndef KIWIX_SERVER_I18N
#define KIWIX_SERVER_I18N
#include "i18n.h"
#include <string>
#include <mustache.hpp>
namespace kiwix
@@ -39,9 +39,36 @@ struct I18nStringTable {
const char* get(const std::string& key) const;
};
std::string getTranslatedString(const std::string& lang, const std::string& key);
namespace i18n
{
typedef kainjow::mustache::object Parameters;
std::string expandParameterizedString(const std::string& lang,
const std::string& key,
const Parameters& params);
class GetTranslatedString
{
public:
explicit GetTranslatedString(const std::string& lang) : m_lang(lang) {}
std::string operator()(const std::string& key) const
{
return getTranslatedString(m_lang, key);
}
std::string operator()(const std::string& key, const Parameters& params) const
{
return expandParameterizedString(m_lang, key, params);
}
private:
const std::string m_lang;
};
class GetTranslatedStringWithMsgId
{
typedef kainjow::mustache::basic_data<std::string> MustacheString;
@@ -66,6 +93,24 @@ private:
} // namespace i18n
struct ParameterizedMessage
{
public: // types
typedef kainjow::mustache::object Parameters;
public: // functions
ParameterizedMessage(const std::string& msgId, const Parameters& params)
: msgId(msgId)
, params(params)
{}
std::string getText(const std::string& lang) const;
private: // data
const std::string msgId;
const Parameters params;
};
struct LangPreference
{
const std::string lang;
@@ -80,4 +125,4 @@ std::string selectMostSuitableLanguage(const UserLangPreferences& prefs);
} // namespace kiwix
#endif // KIWIX_SERVER_I18N_UTILS
#endif // KIWIX_SERVER_I18N

View File

@@ -54,7 +54,7 @@ extern "C" {
#include "search_renderer.h"
#include "opds_dumper.h"
#include "html_dumper.h"
#include "i18n_utils.h"
#include "i18n.h"
#include <zim/uuid.h>
#include <zim/error.h>
@@ -85,18 +85,14 @@ namespace kiwix {
namespace
{
bool ipAvailable(const std::string addr)
inline std::string normalizeRootUrl(std::string rootUrl)
{
auto interfaces = kiwix::getNetworkInterfacesIPv4Or6();
while ( !rootUrl.empty() && rootUrl.back() == '/' )
rootUrl.pop_back();
for (const auto& kv : interfaces) {
const auto& interfaceIps = kv.second;
if ((interfaceIps.addr == addr) || (interfaceIps.addr6 == addr)) {
return true;
}
}
return false;
while ( !rootUrl.empty() && rootUrl.front() == '/' )
rootUrl = rootUrl.substr(1);
return rootUrl.empty() ? rootUrl : "/" + rootUrl;
}
std::string
@@ -115,12 +111,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&) {}
@@ -197,6 +190,12 @@ ParameterizedMessage tooManyBooksMsg(size_t nbBooks, size_t limit)
);
}
ParameterizedMessage nonParameterizedMessage(const std::string& msgId)
{
const ParameterizedMessage::Parameters noParams;
return ParameterizedMessage(msgId, noParams);
}
struct Error : public std::runtime_error {
explicit Error(const ParameterizedMessage& message)
: std::runtime_error("Error while handling request"),
@@ -255,11 +254,6 @@ get_matching_if_none_match_etag(const RequestContext& r, const std::string& etag
}
}
struct NoDelete
{
template<class T> void operator()(T*) {}
};
} // unnamed namespace
std::pair<std::string, Library::BookIdSet> InternalServer::selectBooks(const RequestContext& request) const
@@ -412,9 +406,9 @@ public:
};
InternalServer::InternalServer(LibraryPtr library,
std::shared_ptr<NameMapper> nameMapper,
IpAddress addr,
InternalServer::InternalServer(Library* library,
NameMapper* nameMapper,
std::string addr,
int port,
std::string root,
int nbThreads,
@@ -423,14 +417,11 @@ InternalServer::InternalServer(LibraryPtr library,
bool withTaskbar,
bool withLibraryButton,
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),
@@ -438,17 +429,14 @@ InternalServer::InternalServer(LibraryPtr library,
m_withTaskbar(withTaskbar),
m_withLibraryButton(withLibraryButton),
m_blockExternalLinks(blockExternalLinks),
m_ipMode(ipMode),
m_indexTemplateString(indexTemplateString.empty() ? RESOURCE::templates::index_html : indexTemplateString),
m_ipConnectionLimit(ipConnectionLimit),
mp_daemon(nullptr),
mp_library(library),
mp_nameMapper(nameMapper ? nameMapper : std::shared_ptr<NameMapper>(&defaultNameMapper, NoDelete())),
mp_nameMapper(nameMapper ? nameMapper : &defaultNameMapper),
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);
}
@@ -464,62 +452,28 @@ bool InternalServer::start() {
if (m_verbose.load())
flags |= MHD_USE_DEBUG;
struct sockaddr_in sockAddr4={0};
sockAddr4.sin_family = AF_INET;
sockAddr4.sin_port = htons(m_port);
struct sockaddr_in6 sockAddr6={0};
sockAddr6.sin6_family = AF_INET6;
sockAddr6.sin6_port = htons(m_port);
if (m_addr.addr.empty() && m_addr.addr6.empty()) { // No ip address provided
if (m_ipMode == IpMode::AUTO) m_ipMode = IpMode::ALL;
sockAddr6.sin6_addr = in6addr_any;
sockAddr4.sin_addr.s_addr = htonl(INADDR_ANY);
IpAddress bestIps = kiwix::getBestPublicIps();
if (m_ipMode == IpMode::IPV4 || m_ipMode == IpMode::ALL) m_addr.addr = bestIps.addr;
if (m_ipMode == IpMode::IPV6 || m_ipMode == IpMode::ALL) m_addr.addr6 = bestIps.addr6;
struct sockaddr_in sockAddr;
memset(&sockAddr, 0, sizeof(sockAddr));
sockAddr.sin_family = AF_INET;
sockAddr.sin_port = htons(m_port);
if (m_addr.empty()) {
if (0 != INADDR_ANY) {
sockAddr.sin_addr.s_addr = htonl(INADDR_ANY);
}
m_addr = kiwix::getBestPublicIp();
} else {
const std::string addr = !m_addr.addr.empty() ? m_addr.addr : m_addr.addr6;
if (m_ipMode != kiwix::IpMode::AUTO) {
std::cerr << "ERROR: When an IP address is provided the IP mode must not be set" << std::endl;
if (inet_pton(AF_INET, m_addr.c_str(), &(sockAddr.sin_addr.s_addr)) == 0) {
std::cerr << "Ip address " << m_addr << " is not a valid ip address" << std::endl;
return false;
}
bool validV4 = inet_pton(AF_INET, m_addr.addr.c_str(), &(sockAddr4.sin_addr.s_addr)) == 1;
bool validV6 = inet_pton(AF_INET6, m_addr.addr6.c_str(), &(sockAddr6.sin6_addr.s6_addr)) == 1;
if (!validV4 && !validV6) {
std::cerr << "ERROR: invalid IP address: " << addr << std::endl;
return false;
}
if (!ipAvailable(addr)) {
std::cerr << "ERROR: IP address is not available on this system: " << addr << std::endl;
return false;
}
m_ipMode = !m_addr.addr.empty() ? IpMode::IPV4 : IpMode::IPV6;
}
if (m_ipMode == IpMode::ALL) {
flags|=MHD_USE_DUAL_STACK;
} else if (m_ipMode == IpMode::IPV6) {
flags|=MHD_USE_IPv6;
}
struct sockaddr* sockaddr = (m_ipMode==IpMode::ALL || m_ipMode==IpMode::IPV6)
? (struct sockaddr*)&sockAddr6
: (struct sockaddr*)&sockAddr4;
mp_daemon = MHD_start_daemon(flags,
m_port,
NULL,
NULL,
&staticHandlerCallback,
this,
MHD_OPTION_SOCK_ADDR, sockaddr,
MHD_OPTION_SOCK_ADDR, &sockAddr,
MHD_OPTION_THREAD_POOL_SIZE, m_nbThreads,
MHD_OPTION_PER_IP_CONNECTION_LIMIT, m_ipConnectionLimit,
MHD_OPTION_END);
@@ -560,19 +514,6 @@ static MHD_Result staticHandlerCallback(void* cls,
cont_cls);
}
namespace
{
MHD_Result add_name_value_pair(void *nvp, enum MHD_ValueKind kind,
const char *key, const char *value)
{
auto& nameValuePairs = *reinterpret_cast<RequestContext::NameValuePairs*>(nvp);
nameValuePairs.push_back({key, value});
return MHD_YES;
}
} // unnamed namespace
MHD_Result InternalServer::handlerCallback(struct MHD_Connection* connection,
const char* fullUrl,
const char* method,
@@ -589,10 +530,7 @@ MHD_Result InternalServer::handlerCallback(struct MHD_Connection* connection,
}
const auto url = fullURL2LocalURL(fullUrl, m_rootPrefixOfDecodedURL);
RequestContext::NameValuePairs headers, queryArgs;
MHD_get_connection_values(connection, MHD_HEADER_KIND, add_name_value_pair, &headers);
MHD_get_connection_values(connection, MHD_GET_ARGUMENT_KIND, add_name_value_pair, &queryArgs);
RequestContext request(m_root, url, method, version, headers, queryArgs);
RequestContext request(connection, m_root, url, method, version);
if (m_verbose.load() ) {
request.print_debug_info();
@@ -621,7 +559,7 @@ MHD_Result InternalServer::handlerCallback(struct MHD_Connection* connection,
response->set_etag_body(getLibraryId());
}
auto ret = response->send(request, m_verbose.load(), connection);
auto ret = response->send(request, connection);
auto end_time = std::chrono::steady_clock::now();
auto time_span = std::chrono::duration_cast<std::chrono::duration<double>>(end_time - start_time);
if (m_verbose.load()) {
@@ -650,19 +588,20 @@ std::unique_ptr<Response> InternalServer::handle_request(const RequestContext& r
{
try {
if (! request.is_valid_url()) {
return UrlNotFoundResponse(request);
return HTTP404Response(*this, request)
+ urlNotFoundMsg;
}
if ( request.get_url() == "" ) {
// Redirect /ROOT_LOCATION to /ROOT_LOCATION/ (note the added slash)
// so that relative URLs are resolved correctly
const std::string query = getSearchComponent(request);
return Response::build_redirect(m_root + "/" + query);
return Response::build_redirect(*this, m_root + "/" + query);
}
const ETag etag = get_matching_if_none_match_etag(request, getLibraryId());
if ( etag )
return Response::build_304(etag);
return Response::build_304(*this, etag);
const auto url = request.get_url();
if ( isLocallyCustomizedResource(url) )
@@ -703,13 +642,15 @@ std::unique_ptr<Response> InternalServer::handle_request(const RequestContext& r
const std::string contentUrl = m_root + "/content" + urlEncode(url);
const std::string query = getSearchComponent(request);
return Response::build_redirect(contentUrl + query);
return Response::build_redirect(*this, 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(*this, request)
+ e.what();
} catch (...) {
fprintf(stderr, "===== Unhandled unknown error\n");
return HTTP500Response(request, m_root, request.get_full_url());
return HTTP500Response(*this, request)
+ "Unknown error";
}
}
@@ -722,7 +663,7 @@ MustacheData InternalServer::get_default_data() const
std::unique_ptr<Response> InternalServer::build_homepage(const RequestContext& request)
{
return ContentResponse::build(m_indexTemplateString, get_default_data(), "text/html; charset=utf-8");
return ContentResponse::build(*this, m_indexTemplateString, get_default_data(), "text/html; charset=utf-8");
}
/**
@@ -751,7 +692,8 @@ std::unique_ptr<Response> InternalServer::handle_suggest(const RequestContext& r
}
if ( startsWith(request.get_url(), "/suggest/") ) {
return UrlNotFoundResponse(request);
return HTTP404Response(*this, request)
+ urlNotFoundMsg;
}
std::string bookName, bookId;
@@ -765,7 +707,7 @@ std::unique_ptr<Response> InternalServer::handle_suggest(const RequestContext& r
}
if (archive == nullptr) {
return HTTP404Response(request)
return HTTP404Response(*this, request)
+ noSuchBookErrorMsg(bookName);
}
@@ -800,7 +742,7 @@ std::unique_ptr<Response> InternalServer::handle_suggest(const RequestContext& r
results.addFTSearchSuggestion(request.get_user_language(), queryString);
}
return ContentResponse::build(results.getJSON(), "application/json; charset=utf-8");
return ContentResponse::build(*this, results.getJSON(), "application/json; charset=utf-8");
}
std::unique_ptr<Response> InternalServer::handle_viewer_settings(const RequestContext& request)
@@ -814,7 +756,7 @@ std::unique_ptr<Response> InternalServer::handle_viewer_settings(const RequestCo
{"enable_link_blocking", m_blockExternalLinks ? "true" : "false" },
{"enable_library_button", m_withLibraryButton ? "true" : "false" }
};
return ContentResponse::build(RESOURCE::templates::viewer_settings_js, data, "application/javascript; charset=utf-8");
return ContentResponse::build(*this, RESOURCE::templates::viewer_settings_js, data, "application/javascript; charset=utf-8");
}
std::string InternalServer::getNoJSDownloadPageHTML(const std::string& bookId, const std::string& userLang) const
@@ -840,29 +782,19 @@ 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();
const auto urlParts = kiwix::split(url, "/", true, false);
HTMLDumper htmlDumper(mp_library.get(), mp_nameMapper.get());
HTMLDumper htmlDumper(mp_library, mp_nameMapper);
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();
@@ -879,13 +811,19 @@ std::unique_ptr<Response> InternalServer::handle_no_js(const RequestContext& req
const auto bookId = mp_nameMapper->getIdForName(urlParts[2]);
content = getNoJSDownloadPageHTML(bookId, userLang);
} catch (const std::out_of_range&) {
return UrlNotFoundResponse(request);
return HTTP404Response(*this, request)
+ urlNotFoundMsg;
}
} else {
return UrlNotFoundResponse(request);
return HTTP404Response(*this, request)
+ urlNotFoundMsg;
}
return ContentResponse::build(content, "text/html; charset=utf-8");
return ContentResponse::build(
*this,
content,
"text/html; charset=utf-8"
);
}
namespace
@@ -923,12 +861,14 @@ std::unique_ptr<Response> InternalServer::handle_skin(const RequestContext& requ
try {
const auto accessType = staticResourceAccessType(request, resourceCacheId);
auto response = ContentResponse::build(
*this,
getResource(resourceName),
getMimeTypeForFile(resourceName));
response->set_kind(accessType);
return std::move(response);
} catch (const ResourceNotFound& e) {
return UrlNotFoundResponse(request);
return HTTP404Response(*this, request)
+ urlNotFoundMsg;
}
}
@@ -941,17 +881,20 @@ std::unique_ptr<Response> InternalServer::handle_search(const RequestContext& re
if ( startsWith(request.get_url(), "/search/") ) {
if (request.get_url() == "/search/searchdescription.xml") {
return ContentResponse::build(
*this,
RESOURCE::ft_opensearchdescription_xml,
get_default_data(),
"application/opensearchdescription+xml");
}
return UrlNotFoundResponse(request);
return HTTP404Response(*this, request)
+ urlNotFoundMsg;
}
try {
return handle_search_request(request);
} catch (const Error& e) {
return HTTP400Response(request)
return HTTP400Response(*this, request)
+ invalidUrlMsg
+ e.message();
}
}
@@ -992,12 +935,11 @@ std::unique_ptr<Response> InternalServer::handle_search_request(const RequestCon
} catch(std::runtime_error& e) {
// Searcher->search will throw a runtime error if there is no valid xapian database to do the search.
// (in case of zim file not containing a index)
const auto cssUrl = renderUrl(m_root, RESOURCE::templates::url_of_search_results_css_tmpl);
HTTPErrorResponse response(request, MHD_HTTP_NOT_FOUND,
const auto cssUrl = renderUrl(m_root, RESOURCE::templates::url_of_search_results_css);
HTTPErrorResponse response(*this, request, MHD_HTTP_NOT_FOUND,
"fulltext-search-unavailable",
"404-page-heading",
cssUrl,
/*includeKiwixResponseData=*/true);
cssUrl);
response += nonParameterizedMessage("no-search-results");
// XXX: Now this has to be handled by the iframe-based viewer which
// XXX: has to resolve if the book selection resulted in a single book.
@@ -1011,28 +953,21 @@ 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), mp_nameMapper, mp_library, start,
search->getEstimatedMatches());
renderer.setSearchPattern(searchInfo.pattern);
renderer.setSearchBookQuery(searchInfo.bookFilterQuery);
renderer.setProtocolPrefix(m_root + "/content/");
renderer.setSearchProtocolPrefix(m_root + "/search");
renderer.setPageLength(pageLength);
renderer.setUserLang(request.get_user_language());
if (request.get_requested_format() == "xml") {
return ContentResponse::build(
renderer.getXml(*mp_nameMapper, mp_library.get()),
"application/rss+xml; charset=utf-8"
);
return ContentResponse::build(*this, renderer.getXml(), "application/rss+xml; charset=utf-8");
}
auto response = ContentResponse::build(
renderer.getHtml(*mp_nameMapper, mp_library.get()),
"text/html; charset=utf-8"
);
auto response = ContentResponse::build(*this, renderer.getHtml(), "text/html; charset=utf-8");
// XXX: Now this has to be handled by the iframe-based viewer which
// XXX: has to resolve if the book selection resulted in a single book.
/*
@@ -1052,7 +987,8 @@ std::unique_ptr<Response> InternalServer::handle_random(const RequestContext& re
}
if ( startsWith(request.get_url(), "/random/") ) {
return UrlNotFoundResponse(request);
return HTTP404Response(*this, request)
+ urlNotFoundMsg;
}
std::string bookName;
@@ -1066,7 +1002,7 @@ std::unique_ptr<Response> InternalServer::handle_random(const RequestContext& re
}
if (archive == nullptr) {
return HTTP404Response(request)
return HTTP404Response(*this, request)
+ noSuchBookErrorMsg(bookName);
}
@@ -1074,7 +1010,7 @@ std::unique_ptr<Response> InternalServer::handle_random(const RequestContext& re
auto entry = archive->getRandomEntry();
return build_redirect(bookName, getFinalItem(*archive, entry));
} catch(zim::EntryNotFound& e) {
return HTTP404Response(request)
return HTTP404Response(*this, request)
+ nonParameterizedMessage("random-article-failure");
}
}
@@ -1087,10 +1023,13 @@ std::unique_ptr<Response> InternalServer::handle_captured_external(const Request
} catch (const std::out_of_range& e) {}
if (source.empty()) {
return UrlNotFoundResponse(request);
return HTTP404Response(*this, request)
+ urlNotFoundMsg;
}
return BlockExternalLinkResponse(request, m_root, source);
auto data = get_default_data();
data.set("source", source);
return ContentResponse::build(*this, RESOURCE::templates::captured_external_html, data, "text/html; charset=utf-8");
}
std::unique_ptr<Response> InternalServer::handle_catch(const RequestContext& request)
@@ -1103,14 +1042,15 @@ std::unique_ptr<Response> InternalServer::handle_catch(const RequestContext& req
return handle_captured_external(request);
}
return UrlNotFoundResponse(request);
return HTTP404Response(*this, request)
+ urlNotFoundMsg;
}
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);
@@ -1124,6 +1064,15 @@ InternalServer::search_catalog(const RequestContext& request,
namespace
{
ParameterizedMessage suggestSearchMsg(const std::string& searchURL, const std::string& pattern)
{
return ParameterizedMessage("suggest-search",
{
{ "PATTERN", pattern },
{ "SEARCH_URL", searchURL }
});
}
///////////////////////////////////////////////////////////////////////////////
// The content security policy below is set on responses to the /content
// endpoint in order to prevent the ZIM content from interfering with the
@@ -1141,7 +1090,6 @@ const std::string CONTENT_CSP_HEADER =
"allow-same-origin "
"allow-modals "
"allow-popups "
"allow-popups-to-escape-sandbox "
"allow-forms "
"allow-downloads;";
@@ -1155,7 +1103,7 @@ InternalServer::build_redirect(const std::string& bookName, const zim::Item& ite
{
const auto contentPath = "/content/" + bookName + "/" + item.getPath();
const auto url = m_root + kiwix::urlEncode(contentPath);
return Response::build_redirect(url);
return Response::build_redirect(*this, url);
}
std::unique_ptr<Response> InternalServer::handle_content(const RequestContext& request)
@@ -1178,13 +1126,16 @@ std::unique_ptr<Response> InternalServer::handle_content(const RequestContext& r
} catch (const std::out_of_range& e) {}
if (archive == nullptr) {
return NewHTTP404Response(request, m_root, m_root + url);
const std::string searchURL = m_root + "/search?pattern=" + kiwix::urlEncode(pattern);
return HTTP404Response(*this, request)
+ urlNotFoundMsg
+ suggestSearchMsg(searchURL, kiwix::urlDecode(pattern));
}
const std::string archiveUuid(archive->getUuid());
const ETag etag = get_matching_if_none_match_etag(request, archiveUuid);
if ( etag )
return Response::build_304(etag);
return Response::build_304(*this, etag);
auto urlStr = url.substr(prefixLength + bookName.size());
if (urlStr[0] == '/') {
@@ -1203,7 +1154,7 @@ std::unique_ptr<Response> InternalServer::handle_content(const RequestContext& r
// '-' namespaces, in which case that resource is returned instead.
return build_redirect(bookName, getFinalItem(*archive, entry));
}
auto response = ItemResponse::build(request, entry.getItem());
auto response = ItemResponse::build(*this, request, entry.getItem());
response->set_etag_body(archiveUuid);
if ( !startsWith(entry.getItem().getMimetype(), "application/pdf") ) {
@@ -1223,7 +1174,10 @@ std::unique_ptr<Response> InternalServer::handle_content(const RequestContext& r
if (m_verbose.load())
printf("Failed to find %s\n", urlStr.c_str());
return NewHTTP404Response(request, m_root, m_root + url);
std::string searchURL = m_root + "/search?content=" + bookName + "&pattern=" + kiwix::urlEncode(pattern);
return HTTP404Response(*this, request)
+ urlNotFoundMsg
+ suggestSearchMsg(searchURL, kiwix::urlDecode(pattern));
}
}
@@ -1240,11 +1194,13 @@ std::unique_ptr<Response> InternalServer::handle_raw(const RequestContext& reque
bookName = request.get_url_part(1);
kind = request.get_url_part(2);
} catch (const std::out_of_range& e) {
return UrlNotFoundResponse(request);
return HTTP404Response(*this, request)
+ urlNotFoundMsg;
}
if (kind != "meta" && kind!= "content") {
return UrlNotFoundResponse(request)
return HTTP404Response(*this, request)
+ urlNotFoundMsg
+ invalidRawAccessMsg(kind);
}
@@ -1255,14 +1211,15 @@ std::unique_ptr<Response> InternalServer::handle_raw(const RequestContext& reque
} catch (const std::out_of_range& e) {}
if (archive == nullptr) {
return UrlNotFoundResponse(request)
return HTTP404Response(*this, request)
+ urlNotFoundMsg
+ noSuchBookErrorMsg(bookName);
}
const std::string archiveUuid(archive->getUuid());
const ETag etag = get_matching_if_none_match_etag(request, archiveUuid);
if ( etag )
return Response::build_304(etag);
return Response::build_304(*this, etag);
// Remove the beggining of the path:
// /raw/<bookName>/<kind>/foo
@@ -1273,7 +1230,7 @@ std::unique_ptr<Response> InternalServer::handle_raw(const RequestContext& reque
try {
if (kind == "meta") {
auto item = archive->getMetadataItem(itemPath);
auto response = ItemResponse::build(request, item);
auto response = ItemResponse::build(*this, request, item);
response->set_etag_body(archiveUuid);
return response;
} else {
@@ -1281,7 +1238,7 @@ std::unique_ptr<Response> InternalServer::handle_raw(const RequestContext& reque
if (entry.isRedirect()) {
return build_redirect(bookName, entry.getItem(true));
}
auto response = ItemResponse::build(request, entry.getItem());
auto response = ItemResponse::build(*this, request, entry.getItem());
response->set_etag_body(archiveUuid);
return response;
}
@@ -1289,7 +1246,8 @@ std::unique_ptr<Response> InternalServer::handle_raw(const RequestContext& reque
if (m_verbose.load()) {
printf("Failed to find %s\n", itemPath.c_str());
}
return UrlNotFoundResponse(request)
return HTTP404Response(*this, request)
+ urlNotFoundMsg
+ rawEntryNotFoundMsg(kind, itemPath);
}
}
@@ -1314,10 +1272,12 @@ std::unique_ptr<Response> InternalServer::handle_locally_customized_resource(con
auto byteRange = request.get_range().resolve(resourceData.size());
if (byteRange.kind() != ByteRange::RESOLVED_FULL_CONTENT) {
return Response::build_416(resourceData.size());
return Response::build_416(*this, resourceData.size());
}
return ContentResponse::build(resourceData, crd.mimeType);
return ContentResponse::build(*this,
resourceData,
crd.mimeType);
}
}

View File

@@ -27,7 +27,6 @@ extern "C" {
#include "library.h"
#include "name_mapper.h"
#include "tools.h"
#include <zim/search.h>
#include <zim/suggestion.h>
@@ -90,13 +89,12 @@ class SearchInfo {
typedef kainjow::mustache::data MustacheData;
class OPDSDumper;
class LibraryDumper;
class InternalServer {
public:
InternalServer(LibraryPtr library,
std::shared_ptr<NameMapper> nameMapper,
IpAddress addr,
InternalServer(Library* library,
NameMapper* nameMapper,
std::string addr,
int port,
std::string root,
int nbThreads,
@@ -105,11 +103,8 @@ class InternalServer {
bool withTaskbar,
bool withLibraryButton,
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,
@@ -121,9 +116,8 @@ class InternalServer {
void** cont_cls);
bool start();
void stop();
IpAddress getAddress() const { return m_addr; }
int getPort() const { return m_port; }
IpMode getIpMode() const { return m_ipMode; }
std::string getAddress() { return m_addr; }
int getPort() { return m_port; }
private: // functions
std::unique_ptr<Response> handle_request(const RequestContext& request);
@@ -163,8 +157,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;
@@ -172,7 +164,7 @@ class InternalServer {
typedef ConcurrentCache<std::string, std::shared_ptr<LockableSuggestionSearcher>> SuggestionSearcherCache;
private: // data
IpAddress m_addr;
std::string m_addr;
int m_port;
std::string m_root; // URI-encoded
std::string m_rootPrefixOfDecodedURL; // URI-decoded
@@ -182,13 +174,12 @@ class InternalServer {
bool m_withTaskbar;
bool m_withLibraryButton;
bool m_blockExternalLinks;
IpMode m_ipMode;
std::string m_indexTemplateString;
int m_ipConnectionLimit;
struct MHD_Daemon* mp_daemon;
LibraryPtr mp_library;
std::shared_ptr<NameMapper> mp_nameMapper;
Library* mp_library;
NameMapper* mp_nameMapper;
SearchCache searchCache;
SuggestionSearcherCache suggestionSearcherCache;
@@ -198,8 +189,9 @@ class InternalServer {
class CustomizedResources;
std::unique_ptr<CustomizedResources> m_customizedResources;
const bool m_catalogOnlyMode;
const std::string m_contentServerUrl;
friend std::unique_ptr<Response> Response::build(const InternalServer& server);
friend std::unique_ptr<ContentResponse> ContentResponse::build(const InternalServer& server, const std::string& content, const std::string& mimetype);
friend std::unique_ptr<Response> ItemResponse::build(const InternalServer& server, const RequestContext& request, const zim::Item& item);
};
}

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()) {
@@ -72,7 +63,8 @@ std::unique_ptr<Response> InternalServer::handle_catalog(const RequestContext& r
host = request.get_header("Host");
url = request.get_url_part(1);
} catch (const std::out_of_range&) {
return UrlNotFoundResponse(request);
return HTTP404Response(*this, request)
+ urlNotFoundMsg;
}
if (url == "v2") {
@@ -80,16 +72,19 @@ std::unique_ptr<Response> InternalServer::handle_catalog(const RequestContext& r
}
if (url != "searchdescription.xml" && url != "root.xml" && url != "search") {
return UrlNotFoundResponse(request);
return HTTP404Response(*this, request)
+ urlNotFoundMsg;
}
if (url == "searchdescription.xml") {
auto response = ContentResponse::build(RESOURCE::opensearchdescription_xml, get_default_data(), "application/opensearchdescription+xml");
auto response = ContentResponse::build(*this, RESOURCE::opensearchdescription_xml, get_default_data(), "application/opensearchdescription+xml");
return std::move(response);
}
zim::Uuid uuid;
kiwix::OPDSDumper opdsDumper = getOPDSDumper();
kiwix::OPDSDumper opdsDumper(mp_library, mp_nameMapper);
opdsDumper.setRootLocation(m_root);
opdsDumper.setLibraryId(getLibraryId());
std::vector<std::string> bookIdsToDump;
if (url == "root.xml") {
uuid = zim::Uuid::generate(host);
@@ -100,6 +95,7 @@ std::unique_ptr<Response> InternalServer::handle_catalog(const RequestContext& r
}
auto response = ContentResponse::build(
*this,
opdsDumper.dumpOPDSFeed(bookIdsToDump, request.get_query()),
opdsMimeType[OPDS_ACQUISITION_FEED]);
return std::move(response);
@@ -115,14 +111,15 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2(const RequestContext
try {
url = request.get_url_part(2);
} catch (const std::out_of_range&) {
return UrlNotFoundResponse(request);
return HTTP404Response(*this, request)
+ urlNotFoundMsg;
}
if (url == "root.xml") {
return handle_catalog_v2_root(request);
} else if (url == "searchdescription.xml") {
const std::string endpoint_root = m_root + "/catalog/v2";
return ContentResponse::build(
return ContentResponse::build(*this,
RESOURCE::catalog_v2_searchdescription_xml,
kainjow::mustache::object({{"endpoint_root", endpoint_root}}),
"application/opensearchdescription+xml"
@@ -141,7 +138,8 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2(const RequestContext
} else if (url == "illustration") {
return handle_catalog_v2_illustration(request);
} else {
return UrlNotFoundResponse(request);
return HTTP404Response(*this, request)
+ urlNotFoundMsg;
}
}
@@ -149,6 +147,7 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_root(const RequestCo
{
const std::string libraryId = getLibraryId();
return ContentResponse::build(
*this,
RESOURCE::templates::catalog_v2_root_xml,
kainjow::mustache::object{
{"date", gen_date_str()},
@@ -165,10 +164,13 @@ 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, mp_nameMapper);
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(
*this,
opdsFeed,
opdsMimeType[OPDS_ACQUISITION_FEED]
);
@@ -179,12 +181,16 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_complete_entry(const
try {
mp_library->getBookById(entryId);
} catch (const std::out_of_range&) {
return UrlNotFoundResponse(request);
return HTTP404Response(*this, request)
+ urlNotFoundMsg;
}
kiwix::OPDSDumper opdsDumper = getOPDSDumper();
OPDSDumper opdsDumper(mp_library, mp_nameMapper);
opdsDumper.setRootLocation(m_root);
opdsDumper.setLibraryId(getLibraryId());
const auto opdsFeed = opdsDumper.dumpOPDSCompleteEntry(entryId);
return ContentResponse::build(
*this,
opdsFeed,
opdsMimeType[OPDS_ENTRY]
);
@@ -192,8 +198,11 @@ 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, mp_nameMapper);
opdsDumper.setRootLocation(m_root);
opdsDumper.setLibraryId(getLibraryId());
return ContentResponse::build(
*this,
opdsDumper.categoriesOPDSFeed(),
opdsMimeType[OPDS_NAVIGATION_FEED]
);
@@ -201,8 +210,11 @@ 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, mp_nameMapper);
opdsDumper.setRootLocation(m_root);
opdsDumper.setLibraryId(getLibraryId());
return ContentResponse::build(
*this,
opdsDumper.languagesOPDSFeed(),
opdsMimeType[OPDS_NAVIGATION_FEED]
);
@@ -216,11 +228,13 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_illustration(const R
auto size = request.get_argument<unsigned int>("size");
auto illustration = book.getIllustration(size);
return ContentResponse::build(
*this,
illustration->getData(),
illustration->mimeType
);
} catch(...) {
return UrlNotFoundResponse(request);
return HTTP404Response(*this, request)
+ urlNotFoundMsg;
}
}

View File

@@ -28,7 +28,7 @@
#include <cctype>
#include "tools/stringTools.h"
#include "i18n_utils.h"
#include "i18n.h"
namespace kiwix {
@@ -51,12 +51,11 @@ RequestMethod str2RequestMethod(const std::string& method) {
} // unnamed namespace
RequestContext::RequestContext(const std::string& _rootLocation, // URI-encoded
RequestContext::RequestContext(struct MHD_Connection* connection,
const std::string& _rootLocation, // URI-encoded
const std::string& unrootedUrl, // URI-decoded
const std::string& _method,
const std::string& version,
const NameValuePairs& headers,
const NameValuePairs& queryArgs) :
const std::string& version) :
rootLocation(_rootLocation),
url(unrootedUrl),
method(str2RequestMethod(_method)),
@@ -65,13 +64,9 @@ RequestContext::RequestContext(const std::string& _rootLocation, // URI-encoded
acceptEncodingGzip(false),
byteRange_()
{
for ( const auto& kv : headers ) {
add_header(kv.first, kv.second);
}
for ( const auto& kv : queryArgs ) {
add_argument(kv.first, kv.second);
}
MHD_get_connection_values(connection, MHD_HEADER_KIND, &RequestContext::fill_header, this);
MHD_get_connection_values(connection, MHD_GET_ARGUMENT_KIND, &RequestContext::fill_argument, this);
MHD_get_connection_values(connection, MHD_COOKIE_KIND, &RequestContext::fill_cookie, this);
try {
acceptEncodingGzip =
@@ -88,14 +83,18 @@ RequestContext::RequestContext(const std::string& _rootLocation, // URI-encoded
RequestContext::~RequestContext()
{}
void RequestContext::add_header(const char *key, const char *value)
MHD_Result RequestContext::fill_header(void *__this, enum MHD_ValueKind kind,
const char *key, const char *value)
{
this->headers[lcAll(key)] = value;
RequestContext *_this = static_cast<RequestContext*>(__this);
_this->headers[lcAll(key)] = value;
return MHD_YES;
}
void RequestContext::add_argument(const char *key, const char* value)
MHD_Result RequestContext::fill_argument(void *__this, enum MHD_ValueKind kind,
const char *key, const char* value)
{
RequestContext *_this = this;
RequestContext *_this = static_cast<RequestContext*>(__this);
_this->arguments[key].push_back(value == nullptr ? "" : value);
if ( ! _this->queryString.empty() ) {
_this->queryString += "&";
@@ -105,6 +104,15 @@ void RequestContext::add_argument(const char *key, const char* value)
_this->queryString += "=";
_this->queryString += urlEncode(value);
}
return MHD_YES;
}
MHD_Result RequestContext::fill_cookie(void *__this, enum MHD_ValueKind kind,
const char *key, const char* value)
{
RequestContext *_this = static_cast<RequestContext*>(__this);
_this->cookies[key] = value == nullptr ? "" : value;
return MHD_YES;
}
void RequestContext::print_debug_info() const {
@@ -194,12 +202,21 @@ std::string RequestContext::get_user_language() const
return userlang.lang;
}
bool RequestContext::user_language_comes_from_cookie() const
{
return userlang.selectedBy == UserLanguage::SelectorKind::COOKIE;
}
RequestContext::UserLanguage RequestContext::determine_user_language() const
{
try {
return {UserLanguage::SelectorKind::QUERY_PARAM, get_argument("userlang")};
} catch(const std::out_of_range&) {}
try {
return {UserLanguage::SelectorKind::COOKIE, cookies.at("userlang")};
} catch(const std::out_of_range&) {}
try {
const std::string acceptLanguage = get_header("Accept-Language");
const auto userLangPrefs = parseUserLanguagePreferences(acceptLanguage);

View File

@@ -29,7 +29,7 @@
#include <stdexcept>
#include "byte_range.h"
#include "../tools/stringTools.h"
#include "tools/stringTools.h"
extern "C" {
#include "microhttpd_wrapper.h"
@@ -55,17 +55,12 @@ class IndexError: public std::runtime_error {};
class RequestContext {
public: // types
typedef std::vector<std::pair<const char*, const char*>> NameValuePairs;
public: // functions
RequestContext(const std::string& rootLocation, // URI-encoded
RequestContext(struct MHD_Connection* connection,
const std::string& rootLocation, // URI-encoded
const std::string& unrootedUrl, // URI-decoded
const std::string& method,
const std::string& version,
const NameValuePairs& headers,
const NameValuePairs& queryArgs);
const std::string& version);
~RequestContext();
void print_debug_info() const;
@@ -124,12 +119,15 @@ class RequestContext {
std::string get_user_language() const;
std::string get_requested_format() const;
bool user_language_comes_from_cookie() const;
private: // types
struct UserLanguage
{
enum SelectorKind
{
QUERY_PARAM,
COOKIE,
ACCEPT_LANGUAGE_HEADER,
DEFAULT
};
@@ -150,14 +148,16 @@ class RequestContext {
ByteRange byteRange_;
std::map<std::string, std::string> headers;
std::map<std::string, std::vector<std::string>> arguments;
std::map<std::string, std::string> cookies;
std::string queryString;
UserLanguage userlang;
private: // functions
UserLanguage determine_user_language() const;
void add_header(const char* name, const char* value);
void add_argument(const char* name, const char* value);
static MHD_Result fill_header(void *, enum MHD_ValueKind, const char*, const char*);
static MHD_Result fill_cookie(void *, enum MHD_ValueKind, const char*, const char*);
static MHD_Result fill_argument(void *, enum MHD_ValueKind, const char*, const char*);
};
template<> std::string RequestContext::get_argument(const std::string& name) const;

View File

@@ -32,9 +32,6 @@
#include <zlib.h>
#include <array>
#include <list>
#include <map>
#include <regex>
// This is somehow a magic value.
// If this value is too small, we will compress (and lost cpu time) too much
@@ -50,8 +47,6 @@ namespace kiwix {
namespace
{
typedef kainjow::mustache::data MustacheData;
// some utilities
std::string get_mime_type(const zim::Item& item)
@@ -124,8 +119,9 @@ const char* getCacheControlHeader(Response::Kind k)
} // unnamed namespace
Response::Response()
: m_returnCode(MHD_HTTP_OK)
Response::Response(bool verbose)
: m_verbose(verbose),
m_returnCode(MHD_HTTP_OK)
{
add_header(MHD_HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, "*");
}
@@ -137,14 +133,14 @@ void Response::set_kind(Kind k)
m_etag.set_option(ETag::ZIM_CONTENT);
}
std::unique_ptr<Response> Response::build()
std::unique_ptr<Response> Response::build(const InternalServer& server)
{
return std::make_unique<Response>();
return std::unique_ptr<Response>(new Response(server.m_verbose.load()));
}
std::unique_ptr<Response> Response::build_304(const ETag& etag)
std::unique_ptr<Response> Response::build_304(const InternalServer& server, const ETag& etag)
{
auto response = Response::build();
auto response = Response::build(server);
response->set_code(MHD_HTTP_NOT_MODIFIED);
response->m_etag = etag;
if ( etag.get_option(ETag::ZIM_CONTENT) ) {
@@ -156,316 +152,67 @@ std::unique_ptr<Response> Response::build_304(const ETag& etag)
return response;
}
const UrlNotFoundMsg urlNotFoundMsg;
const InvalidUrlMsg invalidUrlMsg;
namespace
std::string ContentResponseBlueprint::getMessage(const std::string& msgId) const
{
// This class was introduced in order to work around the missing support
// for std::variant (and std::optional) under some of the current build
// platforms.
template<class T>
class Optional
{
public: // functions
Optional() {}
Optional(const T& t) : ptr(new T(t)) {}
Optional(const Optional& o) : ptr(o.has_value() ? new T(*o) : nullptr) {}
Optional(Optional&& o) : ptr(std::move(o.ptr)) {}
Optional& operator=(const Optional& o)
{
*this = Optional(o);
return *this;
}
Optional& operator=(Optional&& o)
{
ptr = std::move(o.ptr);
return *this;
}
bool has_value() const { return ptr.get() != nullptr; }
const T& operator*() const { return *ptr; }
T& operator*() { return *ptr; }
private: // data
std::unique_ptr<T> ptr;
};
} // unnamed namespace
class ContentResponseBlueprint::Data
{
public:
typedef std::list<Data> List;
typedef std::map<std::string, Data> Object;
private:
// std::variant<std::string, bool, List, Object> data;
// XXX: libkiwix is compiled on platforms where std::variant
// XXX: is not yet supported. Hence this hack. Only one
// XXX: of the below data members is expected to contain a value.
Optional<std::string> m_stringValue;
Optional<bool> m_boolValue;
Optional<List> m_listValue;
Optional<Object> m_objectValue;
public:
Data() {}
Data(const std::string& s) : m_stringValue(s) {}
Data(bool b) : m_boolValue(b) {}
Data(const List& l) : m_listValue(l) {}
Data(const Object& o) : m_objectValue(o) {}
MustacheData toMustache(const std::string& lang) const;
Data& operator[](const std::string& key)
{
return (*m_objectValue)[key];
}
void push_back(const Data& d) { (*m_listValue).push_back(d); }
static Data onlyAsNonEmptyValue(const std::string& s)
{
return s.empty() ? Data(false) : Data(s);
}
static Data from(const ParameterizedMessage& pmsg)
{
Object obj;
for(const auto& kv : pmsg.getParams()) {
obj[kv.first] = kv.second;
}
return Object{
{ "msgid", pmsg.getMsgId() },
{ "params", Data(obj) }
};
}
static Data fromMsgId(const std::string& nonParameterizedMsgId)
{
return from(nonParameterizedMessage(nonParameterizedMsgId));
}
static Data staticMultiParagraphText(const std::string& msgIdPrefix, size_t n)
{
Object paragraphs;
for ( size_t i = 1; i <= n; ++i ) {
std::ostringstream oss;
oss << "p" << i;
const std::string pId = oss.str();
paragraphs[pId] = fromMsgId(msgIdPrefix + "." + pId);
}
return paragraphs;
}
std::string asJSON() const;
void dumpJSON(std::ostream& os) const;
private:
bool isString() const { return m_stringValue.has_value(); }
bool isList() const { return m_listValue.has_value(); }
bool isObject() const { return m_objectValue.has_value(); }
const std::string& stringValue() const { return *m_stringValue; }
bool boolValue() const { return *m_boolValue; }
const List& listValue() const { return *m_listValue; }
const Object& objectValue() const { return *m_objectValue; }
const Data* get(const std::string& key) const
{
if ( !isObject() )
return nullptr;
const auto& obj = objectValue();
const auto it = obj.find(key);
return it != obj.end() ? &it->second : nullptr;
}
};
MustacheData ContentResponseBlueprint::Data::toMustache(const std::string& lang) const
{
if ( this->isList() ) {
kainjow::mustache::list l;
for ( const auto& x : this->listValue() ) {
l.push_back(x.toMustache(lang));
}
return l;
} else if ( this->isObject() ) {
const Data* msgId = this->get("msgid");
const Data* msgParams = this->get("params");
if ( msgId && msgId->isString() && msgParams && msgParams->isObject() ) {
std::map<std::string, std::string> params;
for(const auto& kv : msgParams->objectValue()) {
params[kv.first] = kv.second.stringValue();
}
const ParameterizedMessage msg(msgId->stringValue(), ParameterizedMessage::Parameters(params));
return msg.getText(lang);
} else {
kainjow::mustache::object o;
for ( const auto& kv : this->objectValue() ) {
o[kv.first] = kv.second.toMustache(lang);
}
return o;
}
} else if ( this->isString() ) {
return this->stringValue();
} else {
return this->boolValue();
}
return getTranslatedString(m_request.get_user_language(), msgId);
}
void ContentResponseBlueprint::Data::dumpJSON(std::ostream& os) const
{
if ( this->isString() ) {
os << '"' << escapeForJSON(this->stringValue()) << '"';
} else if ( this->isList() ) {
const char * sep = " ";
os << "[";
for ( const auto& x : this->listValue() ) {
os << sep;
x.dumpJSON(os);
sep = ", ";
}
os << " ]";
} else if ( this->isObject() ) {
const char * sep = " ";
os << "{";
for ( const auto& kv : this->objectValue() ) {
os << sep << '"' << kv.first << "\" : ";
kv.second.dumpJSON(os);
sep = ", ";
}
os << " }";
} else {
os << (this->boolValue() ? "true" : "false");
}
}
std::string ContentResponseBlueprint::Data::asJSON() const
{
std::ostringstream oss;
this->dumpJSON(oss);
// This JSON is going to be used in HTML inside a <script></script> tag.
// If it contains "</script>" (or "</script >") as a substring, then the HTML
// parser will be confused. Since for a valid JSON that may happen only inside
// a JSON string, we can safely take advantage of the answers to
// https://stackoverflow.com/questions/28259389/how-to-put-script-in-a-javascript-string
// and work around the issue by inserting an otherwise harmless backslash.
return std::regex_replace(oss.str(), std::regex("</script"), "</scr\\ipt");
}
ContentResponseBlueprint::ContentResponseBlueprint(const RequestContext* request,
int httpStatusCode,
const std::string& mimeType,
const std::string& templateStr,
bool includeKiwixResponseData)
: m_request(*request)
, m_httpStatusCode(httpStatusCode)
, m_mimeType(mimeType)
, m_template(templateStr)
, m_includeKiwixResponseData(includeKiwixResponseData)
, m_data(new Data)
{}
ContentResponseBlueprint::~ContentResponseBlueprint() = default;
std::unique_ptr<ContentResponse> ContentResponseBlueprint::generateResponseObject() const
{
kainjow::mustache::data d = m_data->toMustache(m_request.get_user_language());
if ( m_includeKiwixResponseData ) {
d.set("KIWIX_RESPONSE_TEMPLATE", escapeForJSON(m_template, false));
d.set("KIWIX_RESPONSE_DATA", m_data->asJSON());
}
auto r = ContentResponse::build(m_template, d, m_mimeType);
auto r = ContentResponse::build(m_server, m_template, m_data, m_mimeType);
r->set_code(m_httpStatusCode);
return r;
}
NewHTTP404Response::NewHTTP404Response(const RequestContext& request,
const std::string& root,
const std::string& urlPath)
: ContentResponseBlueprint(&request,
MHD_HTTP_NOT_FOUND,
"text/html; charset=utf-8",
RESOURCE::templates::sexy404_html,
/*includeKiwixResponseData=*/true)
{
*this->m_data = Data(Data::Object{
{"root", root },
{"url_path", urlPath},
{"PAGE_TITLE", Data::fromMsgId("new-404-page-title")},
{"PAGE_HEADING", Data::fromMsgId("new-404-page-heading")},
{"404_img_text", Data::fromMsgId("404-img-text")},
{"path_was_not_found_msg", Data::fromMsgId("path-was-not-found")},
{"advice", Data::staticMultiParagraphText("404-advice", 5)},
});
}
BlockExternalLinkResponse::BlockExternalLinkResponse(const RequestContext& request,
const std::string& root,
const std::string& externalUrl)
: ContentResponseBlueprint(&request,
MHD_HTTP_OK,
"text/html; charset=utf-8",
RESOURCE::templates::captured_external_html,
/*includeKiwixResponseData=*/true)
{
*this->m_data = Data(Data::Object{
{"root", root },
{"external_link_detected", Data::fromMsgId("external-link-detected") },
{"url", externalUrl },
{"caution_warning", Data::fromMsgId("caution-warning") },
{"external_link_intro", Data::fromMsgId("external-link-intro") },
{"advice", Data::staticMultiParagraphText("external-link-advice", 3)},
});
}
HTTPErrorResponse::HTTPErrorResponse(const RequestContext& request,
HTTPErrorResponse::HTTPErrorResponse(const InternalServer& server,
const RequestContext& request,
int httpStatusCode,
const std::string& pageTitleMsgId,
const std::string& headingMsgId,
const std::string& cssUrl,
bool includeKiwixResponseData)
: ContentResponseBlueprint(&request,
const std::string& cssUrl)
: ContentResponseBlueprint(&server,
&request,
httpStatusCode,
request.get_requested_format() == "html" ? "text/html; charset=utf-8" : "application/xml; charset=utf-8",
request.get_requested_format() == "html" ? RESOURCE::templates::error_html : RESOURCE::templates::error_xml,
includeKiwixResponseData)
request.get_requested_format() == "html" ? RESOURCE::templates::error_html : RESOURCE::templates::error_xml)
{
Data::List emptyList;
*this->m_data = Data(Data::Object{
{"CSS_URL", Data::onlyAsNonEmptyValue(cssUrl) },
{"PAGE_TITLE", Data::fromMsgId(pageTitleMsgId)},
{"PAGE_HEADING", Data::fromMsgId(headingMsgId)},
kainjow::mustache::list emptyList;
this->m_data = kainjow::mustache::object{
{"CSS_URL", onlyAsNonEmptyMustacheValue(cssUrl) },
{"PAGE_TITLE", getMessage(pageTitleMsgId)},
{"PAGE_HEADING", getMessage(headingMsgId)},
{"details", emptyList}
});
};
}
HTTP404Response::HTTP404Response(const RequestContext& request)
: HTTPErrorResponse(request,
HTTP404Response::HTTP404Response(const InternalServer& server,
const RequestContext& request)
: HTTPErrorResponse(server,
request,
MHD_HTTP_NOT_FOUND,
"404-page-title",
"404-page-heading",
std::string(),
/*includeKiwixResponseData=*/true)
"404-page-heading")
{
}
UrlNotFoundResponse::UrlNotFoundResponse(const RequestContext& request)
: HTTP404Response(request)
HTTPErrorResponse& HTTP404Response::operator+(UrlNotFoundMsg /*unused*/)
{
const std::string requestUrl = urlDecode(m_request.get_full_url(), false);
*this += ParameterizedMessage("url-not-found", {{"url", requestUrl}});
return *this + ParameterizedMessage("url-not-found", {{"url", requestUrl}});
}
HTTPErrorResponse& HTTPErrorResponse::operator+(const std::string& msg)
{
m_data["details"].push_back({"p", msg});
return *this;
}
HTTPErrorResponse& HTTPErrorResponse::operator+(const ParameterizedMessage& details)
{
(*m_data)["details"].push_back(Data::Object{{"p", Data::from(details)}});
return *this;
return *this + details.getText(m_request.get_user_language());
}
HTTPErrorResponse& HTTPErrorResponse::operator+=(const ParameterizedMessage& details)
@@ -475,51 +222,50 @@ HTTPErrorResponse& HTTPErrorResponse::operator+=(const ParameterizedMessage& det
}
HTTP400Response::HTTP400Response(const RequestContext& request)
: HTTPErrorResponse(request,
HTTP400Response::HTTP400Response(const InternalServer& server,
const RequestContext& request)
: HTTPErrorResponse(server,
request,
MHD_HTTP_BAD_REQUEST,
"400-page-title",
"400-page-heading",
std::string(),
/*includeKiwixResponseData=*/true)
"400-page-heading")
{
}
HTTPErrorResponse& HTTP400Response::operator+(InvalidUrlMsg /*unused*/)
{
std::string requestUrl = urlDecode(m_request.get_full_url(), false);
const auto query = m_request.get_query();
if (!query.empty()) {
requestUrl += "?" + encodeDiples(query);
}
*this += ParameterizedMessage("invalid-request", {{"url", requestUrl}});
kainjow::mustache::mustache msgTmpl(R"(The requested URL "{{{url}}}" is not a valid request.)");
return *this + msgTmpl.render({"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 InternalServer& server,
const RequestContext& request)
: HTTPErrorResponse(server,
request,
MHD_HTTP_INTERNAL_SERVER_ERROR,
"500-page-title",
"500-page-heading")
{
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);
// operator+() is a state-modifying operator (akin to operator+=)
*this + "An internal server error occured. We are sorry about that :/";
}
std::unique_ptr<Response> Response::build_416(size_t resourceLength)
std::unique_ptr<ContentResponse> HTTP500Response::generateResponseObject() const
{
auto response = Response::build();
const std::string mimeType = "text/html;charset=utf-8";
auto r = ContentResponse::build(m_server, m_template, m_data, mimeType);
r->set_code(m_httpStatusCode);
return r;
}
std::unique_ptr<Response> Response::build_416(const InternalServer& server, size_t resourceLength)
{
auto response = Response::build(server);
// [FIXME] (compile with recent enough version of libmicrohttpd)
// response->set_code(MHD_HTTP_RANGE_NOT_SATISFIABLE);
response->set_code(416);
@@ -531,9 +277,9 @@ std::unique_ptr<Response> Response::build_416(size_t resourceLength)
}
std::unique_ptr<Response> Response::build_redirect(const std::string& redirectUrl)
std::unique_ptr<Response> Response::build_redirect(const InternalServer& server, const std::string& redirectUrl)
{
auto response = Response::build();
auto response = Response::build(server);
response->m_returnCode = MHD_HTTP_FOUND;
response->add_header(MHD_HTTP_HEADER_LOCATION, redirectUrl);
return response;
@@ -628,7 +374,7 @@ ContentResponse::create_mhd_response(const RequestContext& request)
return response;
}
MHD_Result Response::send(const RequestContext& request, bool verbose, MHD_Connection* connection)
MHD_Result Response::send(const RequestContext& request, MHD_Connection* connection)
{
MHD_Response* response = create_mhd_response(request);
@@ -641,10 +387,17 @@ MHD_Result Response::send(const RequestContext& request, bool verbose, MHD_Conne
MHD_add_response_header(response, p.first.c_str(), p.second.c_str());
}
if ( ! request.user_language_comes_from_cookie() ) {
const std::string cookie = "userlang=" + request.get_user_language()
+ ";Path=" + request.get_root_path()
+ ";Max-Age=31536000";
MHD_add_response_header(response, MHD_HTTP_HEADER_SET_COOKIE, cookie.c_str());
}
if (m_returnCode == MHD_HTTP_OK && m_byteRange.kind() == ByteRange::RESOLVED_PARTIAL_CONTENT)
m_returnCode = MHD_HTTP_PARTIAL_CONTENT;
if (verbose)
if (m_verbose)
print_response_info(m_returnCode, response);
auto ret = MHD_queue_response(connection, m_returnCode, response);
@@ -652,8 +405,9 @@ MHD_Result Response::send(const RequestContext& request, bool verbose, MHD_Conne
return ret;
}
ContentResponse::ContentResponse(const std::string& content, const std::string& mimetype) :
Response(),
ContentResponse::ContentResponse(const std::string& root, bool verbose, const std::string& content, const std::string& mimetype) :
Response(verbose),
m_root(root),
m_content(content),
m_mimeType(mimetype)
{
@@ -661,23 +415,29 @@ ContentResponse::ContentResponse(const std::string& content, const std::string&
}
std::unique_ptr<ContentResponse> ContentResponse::build(
const InternalServer& server,
const std::string& content,
const std::string& mimetype)
{
return std::make_unique<ContentResponse>(content, mimetype);
return std::unique_ptr<ContentResponse>(new ContentResponse(
server.m_root,
server.m_verbose.load(),
content,
mimetype));
}
std::unique_ptr<ContentResponse> ContentResponse::build(
const InternalServer& server,
const std::string& template_str,
kainjow::mustache::data data,
const std::string& mimetype)
{
auto content = render_template(template_str, data);
return ContentResponse::build(content, mimetype);
return ContentResponse::build(server, content, mimetype);
}
ItemResponse::ItemResponse(const zim::Item& item, const std::string& mimetype, const ByteRange& byterange) :
Response(),
ItemResponse::ItemResponse(bool verbose, const zim::Item& item, const std::string& mimetype, const ByteRange& byterange) :
Response(verbose),
m_item(item),
m_mimeType(mimetype)
{
@@ -686,26 +446,30 @@ ItemResponse::ItemResponse(const zim::Item& item, const std::string& mimetype, c
add_header(MHD_HTTP_HEADER_CONTENT_TYPE, m_mimeType);
}
std::unique_ptr<Response> ItemResponse::build(const RequestContext& request, const zim::Item& item)
std::unique_ptr<Response> ItemResponse::build(const InternalServer& server, const RequestContext& request, const zim::Item& item)
{
const std::string mimetype = get_mime_type(item);
auto byteRange = request.get_range().resolve(item.getSize());
const bool noRange = byteRange.kind() == ByteRange::RESOLVED_FULL_CONTENT;
if (noRange && is_compressible_mime_type(mimetype)) {
// Return a contentResponse
auto response = ContentResponse::build(item.getData(), mimetype);
auto response = ContentResponse::build(server, item.getData(), mimetype);
response->set_kind(Response::ZIM_CONTENT);
response->m_byteRange = byteRange;
return std::move(response);
}
if (byteRange.kind() == ByteRange::RESOLVED_UNSATISFIABLE) {
auto response = Response::build_416(item.getSize());
auto response = Response::build_416(server, item.getSize());
response->set_kind(Response::ZIM_CONTENT);
return response;
}
return std::make_unique<ItemResponse>(item, mimetype, byteRange);
return std::unique_ptr<Response>(new ItemResponse(
server.m_verbose.load(),
item,
mimetype,
byteRange));
}
MHD_Response*

View File

@@ -27,7 +27,7 @@
#include <mustache.hpp>
#include "byte_range.h"
#include "etag.h"
#include "i18n_utils.h"
#include "i18n.h"
#include <zim/item.h>
@@ -41,6 +41,7 @@ class Archive;
namespace kiwix {
class InternalServer;
class RequestContext;
class Response {
@@ -53,15 +54,15 @@ class Response {
};
public:
Response();
Response(bool verbose);
virtual ~Response() = default;
static std::unique_ptr<Response> build();
static std::unique_ptr<Response> build_304(const ETag& etag);
static std::unique_ptr<Response> build_416(size_t resourceLength);
static std::unique_ptr<Response> build_redirect(const std::string& redirectUrl);
static std::unique_ptr<Response> build(const InternalServer& server);
static std::unique_ptr<Response> build_304(const InternalServer& server, const ETag& etag);
static std::unique_ptr<Response> build_416(const InternalServer& server, size_t resourceLength);
static std::unique_ptr<Response> build_redirect(const InternalServer& server, const std::string& redirectUrl);
MHD_Result send(const RequestContext& request, bool verbose, MHD_Connection* connection);
MHD_Result send(const RequestContext& request, MHD_Connection* connection);
void set_code(int code) { m_returnCode = code; }
void set_kind(Kind k);
@@ -77,6 +78,7 @@ class Response {
protected: // data
Kind m_kind = DYNAMIC_CONTENT;
bool m_verbose;
int m_returnCode;
ByteRange m_byteRange;
ETag m_etag;
@@ -89,21 +91,22 @@ class Response {
class ContentResponse : public Response {
public:
ContentResponse(
const std::string& root,
bool verbose,
const std::string& content,
const std::string& mimetype);
static std::unique_ptr<ContentResponse> build(
const InternalServer& server,
const std::string& content,
const std::string& mimetype);
static std::unique_ptr<ContentResponse> build(
const InternalServer& server,
const std::string& template_str,
kainjow::mustache::data data,
const std::string& mimetype);
const std::string& getContent() const { return m_content; }
const std::string& getMimeType() const { return m_mimeType; }
private:
MHD_Response* create_mhd_response(const RequestContext& request);
@@ -111,6 +114,7 @@ class ContentResponse : public Response {
private:
std::string m_root;
std::string m_content;
std::string m_mimeType;
};
@@ -118,80 +122,99 @@ class ContentResponse : public Response {
class ContentResponseBlueprint
{
public: // functions
ContentResponseBlueprint(const RequestContext* request,
ContentResponseBlueprint(const InternalServer* server,
const RequestContext* request,
int httpStatusCode,
const std::string& mimeType,
const std::string& templateStr,
bool includeKiwixResponseData = false);
const std::string& templateStr)
: m_server(*server)
, m_request(*request)
, m_httpStatusCode(httpStatusCode)
, m_mimeType(mimeType)
, m_template(templateStr)
{}
~ContentResponseBlueprint();
virtual ~ContentResponseBlueprint() = default;
operator std::unique_ptr<Response>() const
operator std::unique_ptr<ContentResponse>() const
{
return generateResponseObject();
}
std::unique_ptr<ContentResponse> generateResponseObject() const;
operator std::unique_ptr<Response>() const
{
return operator std::unique_ptr<ContentResponse>();
}
protected: // types
class Data;
protected: //data
protected: // functions
std::string getMessage(const std::string& msgId) const;
virtual std::unique_ptr<ContentResponse> generateResponseObject() const;
public: //data
const InternalServer& m_server;
const RequestContext& m_request;
const int m_httpStatusCode;
const std::string m_mimeType;
const std::string m_template;
const bool m_includeKiwixResponseData;
std::unique_ptr<Data> m_data;
};
struct NewHTTP404Response : ContentResponseBlueprint
{
NewHTTP404Response(const RequestContext& request,
const std::string& root,
const std::string& urlPath);
kainjow::mustache::data m_data;
};
struct HTTPErrorResponse : ContentResponseBlueprint
{
HTTPErrorResponse(const RequestContext& request,
HTTPErrorResponse(const InternalServer& server,
const RequestContext& request,
int httpStatusCode,
const std::string& pageTitleMsgId,
const std::string& headingMsgId,
const std::string& cssUrl = "",
bool includeKiwixResponseData = false);
const std::string& cssUrl = "");
HTTPErrorResponse& operator+(const std::string& msg);
HTTPErrorResponse& operator+(const ParameterizedMessage& errorDetails);
HTTPErrorResponse& operator+=(const ParameterizedMessage& errorDetails);
};
class UrlNotFoundMsg {};
extern const UrlNotFoundMsg urlNotFoundMsg;
struct HTTP404Response : HTTPErrorResponse
{
explicit HTTP404Response(const RequestContext& request);
HTTP404Response(const InternalServer& server,
const RequestContext& request);
using HTTPErrorResponse::operator+;
HTTPErrorResponse& operator+(UrlNotFoundMsg /*unused*/);
};
struct UrlNotFoundResponse : HTTP404Response
{
explicit UrlNotFoundResponse(const RequestContext& request);
};
class InvalidUrlMsg {};
extern const InvalidUrlMsg invalidUrlMsg;
struct HTTP400Response : HTTPErrorResponse
{
explicit HTTP400Response(const RequestContext& request);
HTTP400Response(const InternalServer& server,
const RequestContext& request);
using HTTPErrorResponse::operator+;
HTTPErrorResponse& operator+(InvalidUrlMsg /*unused*/);
};
struct HTTP500Response : ContentResponseBlueprint
struct HTTP500Response : HTTPErrorResponse
{
HTTP500Response(const RequestContext& request,
const std::string& root,
const std::string& urlPath,
const std::string& error = "");
HTTP500Response(const InternalServer& server,
const RequestContext& request);
private: // overrides
// generateResponseObject() is overriden in order to produce a minimal
// response without any need for additional resources from the server
std::unique_ptr<ContentResponse> generateResponseObject() const override;
};
class ItemResponse : public Response {
public:
ItemResponse(const zim::Item& item, const std::string& mimetype, const ByteRange& byterange);
static std::unique_ptr<Response> build(const RequestContext& request, const zim::Item& item);
ItemResponse(bool verbose, const zim::Item& item, const std::string& mimetype, const ByteRange& byterange);
static std::unique_ptr<Response> build(const InternalServer& server, const RequestContext& request, const zim::Item& item);
private:
MHD_Response* create_mhd_response(const RequestContext& request);
@@ -200,13 +223,6 @@ class ItemResponse : public Response {
std::string m_mimeType;
};
struct BlockExternalLinkResponse : ContentResponseBlueprint
{
BlockExternalLinkResponse(const RequestContext& request,
const std::string& root,
const std::string& externalUrl);
};
}
#endif //KIWIXLIB_SERVER_RESPONSE_H

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

@@ -1,86 +0,0 @@
#include "tools.h"
#include "stringTools.h"
#include <mutex>
namespace kiwix
{
namespace
{
// These mappings are not provided by the ICU library, any such mappings can be manually added here
std::map<std::string, std::string> iso639_3 = {
{"ami", "Amis"},
{"atj", "atikamekw"},
{"azb", "آذربایجان دیلی"},
{"bcl", "central bikol"},
{"bgs", "tagabawa"},
{"blk", "ပအိုဝ်ႏ"},
{"bxr", "буряад хэлэн"},
{"cbk", "chavacano"},
{"cdo", "閩東語"},
{"dag", "Dagbani"},
{"diq", "dimli"},
{"dty", "डोटेली"},
{"eml", "emiliân-rumagnōl"},
{"fbs", "српскохрватски"},
{"fon", "fɔ̀ngbè"},
{"gcr", "Kriyòl gwiyannen"},
{"guw", "Gungbe"},
{"hbs", "srpskohrvatski"},
{"hyw", "հայերէն/հայերեն"},
{"ido", "ido"},
{"kbp", "kabɩ"},
{"kld", "Gamilaraay"},
{"lbe", "лакку маз"},
{"lbj", "ལ་དྭགས་སྐད་"},
{"lld", "ladin"},
{"map", "Austronesian"},
{"mhr", "марий йылме"},
{"mnw", "ဘာသာမန်"},
{"myn", "mayan"},
{"nah", "nahuatl"},
{"nai", "north American Indian"},
{"nds", "plattdütsch"},
{"nrm", "bhasa narom"},
{"olo", "livvi"},
{"pih", "Pitcairn-Norfolk"},
{"pnb", "Western Panjabi"},
{"pwn", "Pinayuanan"},
{"rmr", "Caló"},
{"rmy", "romani shib"},
{"roa", "romance languages"},
{"skr", "سرائیکی"},
{"szy", "Sakizaya"},
{"tay", "Tayal"},
{"tgl", "Wikang Tagalog"},
{"twi", "Akwapem Twi"},
// ICU for Ubuntu versions <= focal (20.04) returns "" for the language code ""
// unlike the later versions - which returns "und". We map this value to "Undetermined" for a common ground.
{"", "Undetermined"},
};
std::once_flag fillLanguagesFlag;
void fillLanguagesMap()
{
for (auto icuLangPtr = icu::Locale::getISOLanguages(); *icuLangPtr != NULL; ++icuLangPtr) {
const kiwix::ICULanguageInfo lang(*icuLangPtr);
iso639_3.insert({lang.iso3Code(), lang.selfName()});
}
iso639_3.erase("mul");
}
} // unnamed namespace
std::string getLanguageSelfName(const std::string& lang)
{
std::call_once(fillLanguagesFlag, fillLanguagesMap);
const auto itr = iso639_3.find(lang);
if (itr != iso639_3.end()) {
return itr->second;
}
return lang;
};
} // namespace kiwix

View File

@@ -19,7 +19,6 @@
*/
#include "tools.h"
#include "stringTools.h"
#include <tools/networkTools.h>
#include <stdio.h>
@@ -33,30 +32,17 @@
#include <stdexcept>
#ifdef _WIN32
#include <iphlpapi.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <iostream>
#else
#include <unistd.h>
#include <arpa/inet.h>
#include <ifaddrs.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <net/if.h>
#include <netdb.h>
#endif
#ifdef __HAIKU__
#include <sys/sockio.h>
#endif
namespace kiwix
{
namespace
{
size_t write_callback_to_iss(char* ptr, size_t size, size_t nmemb, void* userdata)
{
auto str = static_cast<std::stringstream*>(userdata);
@@ -64,15 +50,7 @@ size_t write_callback_to_iss(char* ptr, size_t size, size_t nmemb, void* userdat
return nmemb;
}
void updatePublicIpAddress(IpAddress& publicIpAddr, const IpAddress& interfaceIpAddr)
{
if (publicIpAddr.addr.empty()) publicIpAddr.addr = interfaceIpAddr.addr;
if (publicIpAddr.addr6.empty()) publicIpAddr.addr6 = interfaceIpAddr.addr6;
}
} // unnamed namespace
std::string download(const std::string& url) {
std::string kiwix::download(const std::string& url) {
auto curl = curl_easy_init();
std::stringstream ss;
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
@@ -93,161 +71,103 @@ std::string download(const std::string& url) {
return ss.str();
}
namespace
{
std::map<std::string, std::string> kiwix::getNetworkInterfaces() {
std::map<std::string, std::string> interfaces;
#ifdef _WIN32
std::map<std::string, IpAddress> getNetworkInterfacesWin() {
std::map<std::string, IpAddress> interfaces;
const int working_buffer_size = 15000;
const int max_tries = 3;
ULONG flags = GAA_FLAG_INCLUDE_PREFIX;
// default to unspecified address family (both)
ULONG family = AF_UNSPEC;
ULONG outBufLen = working_buffer_size;
ULONG Iterations = 0;
DWORD dwRetVal = 0;
PIP_ADAPTER_ADDRESSES interfacesHead = NULL;
// Successively allocate the required memory until GetAdaptersAddresses does not
// results in ERROR_BUFFER_OVERFLOW for a maximum of max_tries
do{
interfacesHead = (IP_ADAPTER_ADDRESSES *) malloc(outBufLen);
if (interfacesHead == NULL) {
std::cerr << "Memory allocation failed for IP_ADAPTER_ADDRESSES struct" << std::endl;
return interfaces;
}
dwRetVal = GetAdaptersAddresses(family, flags, NULL, interfacesHead, &outBufLen);
} while ((dwRetVal == ERROR_BUFFER_OVERFLOW) && (Iterations < max_tries));
if (dwRetVal == NO_ERROR) {
PIP_ADAPTER_UNICAST_ADDRESS pUnicast = NULL;
unsigned int i = 0;
for (PIP_ADAPTER_ADDRESSES temp = interfacesHead; temp != NULL;
temp = temp->Next) {
pUnicast = temp->FirstUnicastAddress;
if (pUnicast != NULL) {
for (i = 0; pUnicast != NULL; i++){
if (pUnicast->Address.lpSockaddr->sa_family == AF_INET)
{
sockaddr_in *si = (sockaddr_in *)(pUnicast->Address.lpSockaddr);
char host[INET_ADDRSTRLEN]={0};
inet_ntop(AF_INET, &(si->sin_addr), host, sizeof(host));
interfaces[temp->AdapterName].addr=host;
}
else if (pUnicast->Address.lpSockaddr->sa_family == AF_INET6)
{
sockaddr_in6 *si = (sockaddr_in6 *)(pUnicast->Address.lpSockaddr);
char host[INET6_ADDRSTRLEN]={0};
inet_ntop(AF_INET6, &(si->sin6_addr), host, sizeof(host));
if (!IN6_IS_ADDR_LINKLOCAL(&(si->sin6_addr)))
interfaces[temp->AdapterName].addr6=host;
}
pUnicast = pUnicast->Next;
}
}
}
} else {
std::cerr << "Call to GetAdaptersAddresses failed with error: "<< dwRetVal << std::endl;
SOCKET sd = WSASocket(AF_INET, SOCK_DGRAM, 0, 0, 0, 0);
if (sd == INVALID_SOCKET) {
std::cerr << "Failed to get a socket. Error " << WSAGetLastError() << std::endl;
return interfaces;
}
if (interfacesHead) free(interfacesHead);
INTERFACE_INFO InterfaceList[20];
unsigned long nBytesReturned;
if (WSAIoctl(sd, SIO_GET_INTERFACE_LIST, 0, 0, &InterfaceList,
sizeof(InterfaceList), &nBytesReturned, 0, 0) == SOCKET_ERROR) {
std::cerr << "Failed calling WSAIoctl: error " << WSAGetLastError() << std::endl;
return interfaces;
}
int nNumInterfaces = nBytesReturned / sizeof(INTERFACE_INFO);
for (int i = 0; i < nNumInterfaces; ++i) {
sockaddr_in *pAddress;
pAddress = (sockaddr_in *) & (InterfaceList[i].iiAddress.AddressIn);
if(pAddress->sin_family == AF_INET) {
/* Add to the map */
std::string interfaceName = std::string(inet_ntoa(pAddress->sin_addr));
interfaces[interfaceName] = interfaceName;
}
}
#else
/* Get Network interfaces information */
char buf[16384];
struct ifconf ifconf;
int fd = socket(PF_INET, SOCK_DGRAM, 0); /* Only IPV4 */
ifconf.ifc_len = sizeof(buf);
ifconf.ifc_buf=buf;
if(ioctl(fd, SIOCGIFCONF, &ifconf)!=0) {
perror("ioctl(SIOCGIFCONF)");
}
/* Go through each interface */
struct ifreq *ifreq;
ifreq = ifconf.ifc_req;
for (int i = 0; i < ifconf.ifc_len; ) {
if (ifreq->ifr_addr.sa_family == AF_INET) {
/* Get the network interface ip */
char host[128] = { 0 };
const int error = getnameinfo(&(ifreq->ifr_addr), sizeof(ifreq->ifr_addr),
host, sizeof(host),
0, 0, NI_NUMERICHOST);
if (!error) {
std::string interfaceName = std::string(ifreq->ifr_name);
std::string interfaceIp = std::string(host);
/* Add to the map */
interfaces[interfaceName] = interfaceIp;
} else {
perror("getnameinfo()");
}
}
/* some systems have ifr_addr.sa_len and adjust the length that
* way, but not mine. weird */
size_t len;
#ifndef __linux__
len = IFNAMSIZ + ifreq->ifr_addr.sa_len;
#else
len = sizeof(*ifreq);
#endif
ifreq = (struct ifreq*)((char*)ifreq+len);
i += len;
}
#endif
return interfaces;
}
#else
std::string kiwix::getBestPublicIp() {
auto interfaces = getNetworkInterfaces();
std::map<std::string, IpAddress> getNetworkInterfacesPosix() {
std::map<std::string, IpAddress> interfaces;
struct ifaddrs *interfacesHead;
if (getifaddrs(&interfacesHead) == -1) {
perror("getifaddrs");
}
for (ifaddrs *temp = interfacesHead; temp != NULL; temp = temp->ifa_next) {
if (temp->ifa_addr == NULL) continue;
if (temp->ifa_addr->sa_family == AF_INET) {
sockaddr_in *si = (sockaddr_in *)(temp->ifa_addr);
char host[INET_ADDRSTRLEN] = {0};
inet_ntop(AF_INET, &(si->sin_addr), host, sizeof(host));
interfaces[temp->ifa_name].addr=host;
} else if (temp->ifa_addr->sa_family == AF_INET6) {
sockaddr_in6 *si = (sockaddr_in6 *)(temp->ifa_addr);
char host[INET6_ADDRSTRLEN] = {0};
inet_ntop(AF_INET6, &(si->sin6_addr), host, sizeof(host));
if (!IN6_IS_ADDR_LINKLOCAL(&(si->sin6_addr)))
interfaces[temp->ifa_name].addr6=host;
}
}
freeifaddrs(interfacesHead);
return interfaces;
}
#endif
} // unnamed namespace
std::map<std::string, IpAddress> getNetworkInterfacesIPv4Or6() {
#ifdef _WIN32
return getNetworkInterfacesWin();
#else
return getNetworkInterfacesPosix();
#endif
}
std::map<std::string, std::string> getNetworkInterfaces() {
std::map<std::string, std::string> result;
for ( const auto& kv : getNetworkInterfacesIPv4Or6() ) {
const std::string& interfaceName = kv.first;
const auto& ipAddresses = kv.second;
if ( !ipAddresses.addr.empty() ) {
result[interfaceName] = ipAddresses.addr;
}
}
return result;
}
IpAddress getBestPublicIps() {
IpAddress bestPublicIps;
std::map<std::string, IpAddress> interfaces = getNetworkInterfacesIPv4Or6();
#ifndef _WIN32
const char* const prioritizedNames[] = { "eth0", "eth1", "wlan0", "wlan1", "en0", "en1" };
for (const auto& name : prioritizedNames) {
const auto it = interfaces.find(name);
if (it != interfaces.end()) {
updatePublicIpAddress(bestPublicIps, it->second);
const char* const prioritizedNames[] =
{ "eth0", "eth1", "wlan0", "wlan1", "en0", "en1" };
for(auto name: prioritizedNames) {
auto it = interfaces.find(name);
if(it != interfaces.end()) {
return it->second;
}
}
#endif
const char* const v4prefixes[] = { "192.168", "172.16", "10.0" };
for (const auto& prefix : v4prefixes) {
for (const auto& kv : interfaces) {
const auto& interfaceIps = kv.second;
if (kiwix::startsWith(interfaceIps.addr, prefix)) {
updatePublicIpAddress(bestPublicIps, interfaceIps);
const char* const prefixes[] = { "192.168", "172.16.", "10.0" };
for(auto prefix : prefixes){
for(auto& itr : interfaces) {
auto interfaceIp = itr.second;
if (interfaceIp.find(prefix) == 0) {
return interfaceIp;
}
}
}
updatePublicIpAddress(bestPublicIps, {"127.0.0.1", "::1"});
return bestPublicIps;
return "127.0.0.1";
}
std::string getBestPublicIp()
{
return getBestPublicIps().addr;
}
} // namespace kiwix

View File

@@ -1,70 +0,0 @@
#include "tools.h"
#include <pugixml.hpp>
namespace kiwix
{
namespace
{
#define VALUE(name) entryNode.child(name).child_value()
FeedLanguages parseLanguages(const pugi::xml_document& doc)
{
pugi::xml_node feedNode = doc.child("feed");
FeedLanguages langs;
for (pugi::xml_node entryNode = feedNode.child("entry"); entryNode;
entryNode = entryNode.next_sibling("entry")) {
auto title = VALUE("title");
auto code = VALUE("dc:language");
langs.push_back({code, title});
}
return langs;
}
FeedCategories parseCategories(const pugi::xml_document& doc)
{
pugi::xml_node feedNode = doc.child("feed");
FeedCategories categories;
for (pugi::xml_node entryNode = feedNode.child("entry"); entryNode;
entryNode = entryNode.next_sibling("entry")) {
auto title = VALUE("title");
categories.push_back(title);
}
return categories;
}
} // unnamed namespace
FeedLanguages readLanguagesFromFeed(const std::string& content)
{
pugi::xml_document doc;
pugi::xml_parse_result result
= doc.load_buffer((void*)content.data(), content.size());
if (result) {
auto langs = parseLanguages(doc);
return langs;
}
return FeedLanguages();
}
FeedCategories readCategoriesFromFeed(const std::string& content)
{
pugi::xml_document doc;
pugi::xml_parse_result result
= doc.load_buffer((void*)content.data(), content.size());
FeedCategories categories;
if (result) {
categories = parseCategories(doc);
return categories;
}
return categories;
}
} // namespace kiwix

View File

@@ -32,7 +32,7 @@
#endif
#include "tools/stringTools.h"
#include "server/i18n_utils.h"
#include "server/i18n.h"
#include "libkiwix-resources.h"
#include <map>
@@ -327,37 +327,22 @@ std::string kiwix::render_template(const std::string& template_str, kainjow::mus
return ss.str();
}
// The escapeQuote parameter of escapeForJSON() defaults to true.
// This constant makes the calls to escapeForJSON() where the quote symbol
// should not be escaped (as it is later replaced with the HTML character entity
// &quot;) more readable.
static const bool DONT_ESCAPE_QUOTE = false;
std::string kiwix::escapeForJSON(const std::string& s, bool escapeQuote)
{
std::ostringstream oss;
for (char c : s) {
if ( c == '\\' ) {
oss << "\\\\";
} else if ( unsigned(c) < 0x20U ) {
switch ( c ) {
case '\n': oss << "\\n"; break;
case '\r': oss << "\\r"; break;
case '\t': oss << "\\t"; break;
default: oss << "\\u" << std::setw(4) << std::setfill('0') << unsigned(c);
}
} else if ( c == '"' && escapeQuote ) {
oss << "\\\"";
} else {
oss << c;
}
}
return oss.str();
}
namespace
{
std::string escapeBackslashes(const std::string& s)
{
std::string es;
es.reserve(s.size());
for (char c : s) {
if ( c == '\\' ) {
es.push_back('\\');
}
es.push_back(c);
}
return es;
}
std::string makeFulltextSearchSuggestion(const std::string& lang,
const std::string& queryString)
{
@@ -383,10 +368,10 @@ void kiwix::Suggestions::add(const zim::SuggestionItem& suggestion)
? suggestion.getSnippet()
: suggestion.getTitle();
result.set("label", escapeForJSON(label, DONT_ESCAPE_QUOTE));
result.set("value", escapeForJSON(suggestion.getTitle(), DONT_ESCAPE_QUOTE));
result.set("label", escapeBackslashes(label));
result.set("value", escapeBackslashes(suggestion.getTitle()));
result.set("kind", "path");
result.set("path", escapeForJSON(suggestion.getPath(), DONT_ESCAPE_QUOTE));
result.set("path", escapeBackslashes(suggestion.getPath()));
result.set("first", m_data.is_empty_list());
m_data.push_back(result);
}
@@ -396,8 +381,8 @@ void kiwix::Suggestions::addFTSearchSuggestion(const std::string& uiLang,
{
kainjow::mustache::data result;
const std::string label = makeFulltextSearchSuggestion(uiLang, queryString);
result.set("label", escapeForJSON(label, DONT_ESCAPE_QUOTE));
result.set("value", escapeForJSON(queryString + " ", DONT_ESCAPE_QUOTE));
result.set("label", escapeBackslashes(label));
result.set("value", escapeBackslashes(queryString + " "));
result.set("kind", "pattern");
result.set("first", m_data.is_empty_list());
m_data.push_back(result);

View File

@@ -320,6 +320,16 @@ bool kiwix::fileReadable(const std::string& path)
#endif
}
bool makeDirectory(const std::string& path)
{
#ifdef _WIN32
int status = _wmkdir(Utf8ToWide(path).c_str());
#else
int status = mkdir(path.c_str(), S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH);
#endif
return status == 0;
}
std::string makeTmpDirectory()
{
#ifdef _WIN32
@@ -428,6 +438,52 @@ std::string kiwix::getCurrentDirectory()
return ret;
}
std::string kiwix::getDataDirectory()
{
// Try to get the dataDir from the `KIWIX_DATA_DIR` env var
#ifdef _WIN32
wchar_t* cDataDir = ::_wgetenv(L"KIWIX_DATA_DIR");
if (cDataDir != nullptr) {
return WideToUtf8(cDataDir);
}
#else
char* cDataDir = ::getenv("KIWIX_DATA_DIR");
if (cDataDir != nullptr) {
return cDataDir;
}
#endif
// Compute the dataDir from the user directory.
std::string dataDir;
#ifdef _WIN32
cDataDir = ::_wgetenv(L"APPDATA");
if (cDataDir == nullptr)
cDataDir = ::_wgetenv(L"USERPROFILE");
if (cDataDir != nullptr)
dataDir = WideToUtf8(cDataDir);
#else
cDataDir = ::getenv("XDG_DATA_HOME");
if (cDataDir != nullptr) {
dataDir = cDataDir;
} else {
cDataDir = ::getenv("HOME");
if (cDataDir != nullptr) {
dataDir = cDataDir;
dataDir = appendToDirectory(dataDir, ".local");
dataDir = appendToDirectory(dataDir, "share");
}
}
#endif
if (!dataDir.empty()) {
dataDir = appendToDirectory(dataDir, "kiwix");
makeDirectory(dataDir);
return dataDir;
}
// Let's use the currentDirectory
return getCurrentDirectory();
}
static std::map<std::string, std::string> extMimeTypes = {
{ "html", "text/html"},
{ "htm", "text/html"},

View File

@@ -29,6 +29,7 @@ std::wstring Utf8ToWide(const std::string& str);
unsigned int getFileSize(const std::string& path);
std::string getFileSizeAsString(const std::string& path);
bool makeDirectory(const std::string& path);
std::string makeTmpDirectory();
bool copyFile(const std::string& sourcePath, const std::string& destPath);
bool writeTextFile(const std::string& path, const std::string& content);

View File

@@ -32,7 +32,6 @@
#include <iostream>
#include <iomanip>
#include <regex>
/* tell ICU where to find its dat file (tables) */
void kiwix::loadICUExternalTables()
@@ -257,7 +256,7 @@ std::string kiwix::urlDecode(const std::string& value, bool component)
// If there aren't enough characters left for this to be a
// valid escape code, just use the character and move on
if (value.end() - it < 3) {
if (it > value.end() - 3) {
os << *it;
continue;
}
@@ -440,13 +439,3 @@ template<>
std::string kiwix::extractFromString(const std::string& str) {
return str;
}
std::string kiwix::getSlugifiedFileName(const std::string& filename)
{
#ifdef _WIN32
const std::regex reservedCharsReg(R"([<>:"/\\|?*])");
#else
const std::regex reservedCharsReg("/");
#endif
return std::regex_replace(filename, reservedCharsReg, "_");
}

View File

@@ -31,6 +31,7 @@
namespace kiwix
{
std::string beautifyInteger(uint64_t number);
std::string beautifyFileSize(uint64_t number);
void printStringInHexadecimal(const char* s);
void printStringInHexadecimal(icu::UnicodeString s);
void stringReplacement(std::string& str,
@@ -53,7 +54,6 @@ private:
const icu::Locale locale;
};
std::string escapeForJSON(const std::string& s, bool escapeQuote = true);
/* urlEncode() is the equivalent of JS encodeURIComponent(), with the only
* difference that the slash (/) symbol is NOT encoded. */
@@ -66,9 +66,6 @@ std::string ucAll(const std::string& word);
std::string lcAll(const std::string& word);
std::string ucFirst(const std::string& word);
std::string lcFirst(const std::string& word);
/* This function is broken, related Github issue
* https://github.com/kiwix/libkiwix/issues/1188 */
std::string toTitle(const std::string& word);
std::string normalize(const std::string& word);

View File

@@ -30,10 +30,7 @@ def get_translation_info(filepath):
with open(filepath, 'r', encoding="utf-8") as f:
content = json.load(f)
lang_name = content.get("name")
translation_count = len(content)
return dict(iso_code=lang_code,
self_name=lang_name,
translation_count=translation_count)
return lang_code, lang_name
language_list = []
json_files = translation_dir.glob("*.json")
@@ -43,14 +40,14 @@ with open(resource_file, 'w', encoding="utf-8") as f:
continue
print("Processing", i18n_file.name)
if i18n_file.name != "test.json":
translation_info = get_translation_info(i18n_file)
lang_name = translation_info["self_name"]
lang_code, lang_name = get_translation_info(i18n_file)
if lang_name:
language_list.append(translation_info)
language_list.append((lang_code, lang_name))
else:
print(f"Warning: missing 'name' in {i18n_file.name}")
f.write(str(i18n_file.relative_to(script_path.parent)) + '\n')
language_list = [{name: code} for code, name in sorted(language_list)]
language_list_jsobj_str = json.dumps(language_list,
indent=2,
ensure_ascii=False)

View File

@@ -1,47 +1,29 @@
skin/i18n/ar.json
skin/i18n/bn.json
skin/i18n/br.json
skin/i18n/cs.json
skin/i18n/dag.json
skin/i18n/de.json
skin/i18n/dga.json
skin/i18n/el.json
skin/i18n/en.json
skin/i18n/es.json
skin/i18n/fi.json
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
skin/i18n/ig.json
skin/i18n/it.json
skin/i18n/ja.json
skin/i18n/ko.json
skin/i18n/ku-latn.json
skin/i18n/lb.json
skin/i18n/mk.json
skin/i18n/ms.json
skin/i18n/nb.json
skin/i18n/nl.json
skin/i18n/nqo.json
skin/i18n/or.json
skin/i18n/pl.json
skin/i18n/pt-br.json
skin/i18n/pt.json
skin/i18n/ro.json
skin/i18n/ru.json
skin/i18n/sc.json
skin/i18n/sk.json
skin/i18n/skr-arab.json
skin/i18n/sl.json
skin/i18n/sq.json
skin/i18n/sv.json
skin/i18n/sw.json
skin/i18n/te.json
skin/i18n/test.json
skin/i18n/tr.json
skin/i18n/zh-hans.json

View File

@@ -1,30 +1,21 @@
skin/caret.png
skin/bittorrent.png
skin/magnet.png
skin/404.svg
skin/500.svg
skin/blocklink.svg
skin/feed.svg
skin/langSelector.svg
skin/download.png
skin/download-white.svg
skin/hash.png
skin/search-icon.svg
skin/iso6391To3.js
skin/isotope.pkgd.min.js
skin/index.js
skin/autoComplete/autoComplete.min.js
skin/error.css
skin/print.css
skin/kiwix.css
skin/autoComplete.min.js
skin/taskbar.css
skin/index.css
skin/fonts/DMSans-Regular.ttf
skin/fonts/Poppins.ttf
skin/fonts/Roboto.ttf
skin/search_results.css
skin/blank.html
skin/polyfills.js
skin/viewer.js
skin/i18n.js
skin/languages.js
@@ -44,16 +35,14 @@ templates/catalog_v2_entry.xml
templates/catalog_v2_partial_entry.xml
templates/catalog_v2_categories.xml
templates/catalog_v2_languages.xml
templates/url_of_search_results_css.tmpl
templates/url_of_search_results_css
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
skin/autoComplete/css/autoComplete.css
skin/css/autoComplete.css
skin/favicon/android-chrome-192x192.png
skin/favicon/android-chrome-512x512.png
skin/favicon/apple-touch-icon.png

View File

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.4 KiB

View File

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 9.7 KiB

View File

@@ -1,201 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -1,654 +0,0 @@
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.autoComplete = factory());
}(this, (function () { 'use strict';
function ownKeys(object, enumerableOnly) {
var keys = Object.keys(object);
if (Object.getOwnPropertySymbols) {
var symbols = Object.getOwnPropertySymbols(object);
if (enumerableOnly) {
symbols = symbols.filter(function (sym) {
return Object.getOwnPropertyDescriptor(object, sym).enumerable;
});
}
keys.push.apply(keys, symbols);
}
return keys;
}
function _objectSpread2(target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i] != null ? arguments[i] : {};
if (i % 2) {
ownKeys(Object(source), true).forEach(function (key) {
_defineProperty(target, key, source[key]);
});
} else if (Object.getOwnPropertyDescriptors) {
Object.defineProperties(target, Object.getOwnPropertyDescriptors(source));
} else {
ownKeys(Object(source)).forEach(function (key) {
Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key));
});
}
}
return target;
}
function _typeof(obj) {
"@babel/helpers - typeof";
if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {
_typeof = function (obj) {
return typeof obj;
};
} else {
_typeof = function (obj) {
return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
};
}
return _typeof(obj);
}
function _defineProperty(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, {
value: value,
enumerable: true,
configurable: true,
writable: true
});
} else {
obj[key] = value;
}
return obj;
}
function _toConsumableArray(arr) {
return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread();
}
function _arrayWithoutHoles(arr) {
if (Array.isArray(arr)) return _arrayLikeToArray(arr);
}
function _iterableToArray(iter) {
if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter);
}
function _unsupportedIterableToArray(o, minLen) {
if (!o) return;
if (typeof o === "string") return _arrayLikeToArray(o, minLen);
var n = Object.prototype.toString.call(o).slice(8, -1);
if (n === "Object" && o.constructor) n = o.constructor.name;
if (n === "Map" || n === "Set") return Array.from(o);
if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen);
}
function _arrayLikeToArray(arr, len) {
if (len == null || len > arr.length) len = arr.length;
for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i];
return arr2;
}
function _nonIterableSpread() {
throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}
function _createForOfIteratorHelper(o, allowArrayLike) {
var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"];
if (!it) {
if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") {
if (it) o = it;
var i = 0;
var F = function () {};
return {
s: F,
n: function () {
if (i >= o.length) return {
done: true
};
return {
done: false,
value: o[i++]
};
},
e: function (e) {
throw e;
},
f: F
};
}
throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}
var normalCompletion = true,
didErr = false,
err;
return {
s: function () {
it = it.call(o);
},
n: function () {
var step = it.next();
normalCompletion = step.done;
return step;
},
e: function (e) {
didErr = true;
err = e;
},
f: function () {
try {
if (!normalCompletion && it.return != null) it.return();
} finally {
if (didErr) throw err;
}
}
};
}
var select$1 = function select(element) {
return typeof element === "string" ? document.querySelector(element) : element();
};
var create = function create(tag, options) {
var el = typeof tag === "string" ? document.createElement(tag) : tag;
for (var key in options) {
var val = options[key];
if (key === "inside") {
val.append(el);
} else if (key === "dest") {
select$1(val[0]).insertAdjacentElement(val[1], el);
} else if (key === "around") {
var ref = val;
ref.parentNode.insertBefore(el, ref);
el.append(ref);
if (ref.getAttribute("autofocus") != null) ref.focus();
} else if (key in el) {
el[key] = val;
} else {
el.setAttribute(key, val);
}
}
return el;
};
var getQuery = function getQuery(field) {
return field instanceof HTMLInputElement || field instanceof HTMLTextAreaElement ? field.value : field.innerHTML;
};
var format = function format(value, diacritics) {
value = value.toString().toLowerCase();
return diacritics ? value.normalize("NFD").replace(/[\u0300-\u036f]/g, "").normalize("NFC") : value;
};
var debounce = function debounce(callback, duration) {
var timer;
return function () {
clearTimeout(timer);
timer = setTimeout(function () {
return callback();
}, duration);
};
};
var checkTrigger = function checkTrigger(query, condition, threshold) {
return condition ? condition(query) : query.length >= threshold;
};
var mark = function mark(value, cls) {
return create("mark", _objectSpread2({
innerHTML: value
}, typeof cls === "string" && {
"class": cls
})).outerHTML;
};
var configure = (function (ctx) {
var name = ctx.name,
options = ctx.options,
resultsList = ctx.resultsList,
resultItem = ctx.resultItem;
for (var option in options) {
if (_typeof(options[option]) === "object") {
if (!ctx[option]) ctx[option] = {};
for (var subOption in options[option]) {
ctx[option][subOption] = options[option][subOption];
}
} else {
ctx[option] = options[option];
}
}
ctx.selector = ctx.selector || "#" + name;
resultsList.destination = resultsList.destination || ctx.selector;
resultsList.id = resultsList.id || name + "_list_" + ctx.id;
resultItem.id = resultItem.id || name + "_result";
ctx.input = select$1(ctx.selector);
});
var eventEmitter = (function (name, ctx) {
ctx.input.dispatchEvent(new CustomEvent(name, {
bubbles: true,
detail: ctx.feedback,
cancelable: true
}));
});
var search = (function (query, record, options) {
var _ref = options || {},
mode = _ref.mode,
diacritics = _ref.diacritics,
highlight = _ref.highlight;
var nRecord = format(record, diacritics);
record = record.toString();
query = format(query, diacritics);
if (mode === "loose") {
query = query.replace(/ /g, "");
var qLength = query.length;
var cursor = 0;
var match = Array.from(record).map(function (character, index) {
if (cursor < qLength && nRecord[index] === query[cursor]) {
character = highlight ? mark(character, highlight) : character;
cursor++;
}
return character;
}).join("");
if (cursor === qLength) return match;
} else {
var _match = nRecord.indexOf(query);
if (~_match) {
query = record.substring(_match, _match + query.length);
_match = highlight ? record.replace(query, mark(query, highlight)) : record;
return _match;
}
}
});
var getData = function getData(ctx, query) {
return new Promise(function ($return, $error) {
var data;
data = ctx.data;
if (data.cache && data.store) return $return();
return new Promise(function ($return, $error) {
if (typeof data.src === "function") {
return data.src(query).then($return, $error);
}
return $return(data.src);
}).then(function ($await_4) {
try {
ctx.feedback = data.store = $await_4;
eventEmitter("response", ctx);
return $return();
} catch ($boundEx) {
return $error($boundEx);
}
}, $error);
});
};
var findMatches = function findMatches(query, ctx) {
var data = ctx.data,
searchEngine = ctx.searchEngine;
var matches = [];
data.store.forEach(function (value, index) {
var find = function find(key) {
var record = key ? value[key] : value;
var match = typeof searchEngine === "function" ? searchEngine(query, record) : search(query, record, {
mode: searchEngine,
diacritics: ctx.diacritics,
highlight: ctx.resultItem.highlight
});
if (!match) return;
var result = {
match: match,
value: value
};
if (key) result.key = key;
matches.push(result);
};
if (data.keys) {
var _iterator = _createForOfIteratorHelper(data.keys),
_step;
try {
for (_iterator.s(); !(_step = _iterator.n()).done;) {
var key = _step.value;
find(key);
}
} catch (err) {
_iterator.e(err);
} finally {
_iterator.f();
}
} else {
find();
}
});
if (data.filter) matches = data.filter(matches);
var results = matches.slice(0, ctx.resultsList.maxResults);
ctx.feedback = {
query: query,
matches: matches,
results: results
};
eventEmitter("results", ctx);
};
var Expand = "aria-expanded";
var Active = "aria-activedescendant";
var Selected = "aria-selected";
var feedback = function feedback(ctx, index) {
ctx.feedback.selection = _objectSpread2({
index: index
}, ctx.feedback.results[index]);
};
var render = function render(ctx) {
var resultsList = ctx.resultsList,
list = ctx.list,
resultItem = ctx.resultItem,
feedback = ctx.feedback;
var matches = feedback.matches,
results = feedback.results;
ctx.cursor = -1;
list.innerHTML = "";
if (matches.length || resultsList.noResults) {
var fragment = new DocumentFragment();
results.forEach(function (result, index) {
var element = create(resultItem.tag, _objectSpread2({
id: "".concat(resultItem.id, "_").concat(index),
role: "option",
innerHTML: result.match,
inside: fragment
}, resultItem["class"] && {
"class": resultItem["class"]
}));
if (resultItem.element) resultItem.element(element, result);
});
list.append(fragment);
if (resultsList.element) resultsList.element(list, feedback);
open(ctx);
} else {
close(ctx);
}
};
var open = function open(ctx) {
if (ctx.isOpen) return;
(ctx.wrapper || ctx.input).setAttribute(Expand, true);
ctx.list.removeAttribute("hidden");
ctx.isOpen = true;
eventEmitter("open", ctx);
};
var close = function close(ctx) {
if (!ctx.isOpen) return;
(ctx.wrapper || ctx.input).setAttribute(Expand, false);
ctx.input.setAttribute(Active, "");
ctx.list.setAttribute("hidden", "");
ctx.isOpen = false;
eventEmitter("close", ctx);
};
var goTo = function goTo(index, ctx) {
var resultItem = ctx.resultItem;
var results = ctx.list.getElementsByTagName(resultItem.tag);
var cls = resultItem.selected ? resultItem.selected.split(" ") : false;
if (ctx.isOpen && results.length) {
var _results$index$classL;
var state = ctx.cursor;
if (index >= results.length) index = 0;
if (index < 0) index = results.length - 1;
ctx.cursor = index;
if (state > -1) {
var _results$state$classL;
results[state].removeAttribute(Selected);
if (cls) (_results$state$classL = results[state].classList).remove.apply(_results$state$classL, _toConsumableArray(cls));
}
results[index].setAttribute(Selected, true);
if (cls) (_results$index$classL = results[index].classList).add.apply(_results$index$classL, _toConsumableArray(cls));
ctx.input.setAttribute(Active, results[ctx.cursor].id);
ctx.list.scrollTop = results[index].offsetTop - ctx.list.clientHeight + results[index].clientHeight + 5;
ctx.feedback.cursor = ctx.cursor;
feedback(ctx, index);
eventEmitter("navigate", ctx);
}
};
var next = function next(ctx) {
goTo(ctx.cursor + 1, ctx);
};
var previous = function previous(ctx) {
goTo(ctx.cursor - 1, ctx);
};
var select = function select(ctx, event, index) {
index = index >= 0 ? index : ctx.cursor;
if (index < 0) return;
ctx.feedback.event = event;
feedback(ctx, index);
eventEmitter("selection", ctx);
close(ctx);
};
var click = function click(event, ctx) {
var itemTag = ctx.resultItem.tag.toUpperCase();
var items = Array.from(ctx.list.querySelectorAll(itemTag));
var item = event.target.closest(itemTag);
if (item && item.nodeName === itemTag) {
select(ctx, event, items.indexOf(item));
}
};
var navigate = function navigate(event, ctx) {
switch (event.keyCode) {
case 40:
case 38:
event.preventDefault();
event.keyCode === 40 ? next(ctx) : previous(ctx);
break;
case 13:
if (!ctx.submit) event.preventDefault();
if (ctx.cursor >= 0) select(ctx, event);
break;
case 9:
if (ctx.resultsList.tabSelect && ctx.cursor >= 0) select(ctx, event);
break;
case 27:
ctx.input.value = "";
close(ctx);
break;
}
};
function start (ctx, q) {
var _this = this;
return new Promise(function ($return, $error) {
var queryVal, condition;
queryVal = q || getQuery(ctx.input);
queryVal = ctx.query ? ctx.query(queryVal) : queryVal;
condition = checkTrigger(queryVal, ctx.trigger, ctx.threshold);
if (condition) {
return getData(ctx, queryVal).then(function ($await_2) {
try {
if (ctx.feedback instanceof Error) return $return();
findMatches(queryVal, ctx);
if (ctx.resultsList) render(ctx);
return $If_1.call(_this);
} catch ($boundEx) {
return $error($boundEx);
}
}, $error);
} else {
close(ctx);
return $If_1.call(_this);
}
function $If_1() {
return $return();
}
});
}
var eventsManager = function eventsManager(events, callback) {
for (var element in events) {
for (var event in events[element]) {
callback(element, event);
}
}
};
var addEvents = function addEvents(ctx) {
var events = ctx.events;
var run = debounce(function () {
return start(ctx);
}, ctx.debounce);
var publicEvents = ctx.events = _objectSpread2({
input: _objectSpread2({}, events && events.input)
}, ctx.resultsList && {
list: events ? _objectSpread2({}, events.list) : {}
});
var privateEvents = {
input: {
input: function input() {
run();
},
keydown: function keydown(event) {
navigate(event, ctx);
},
blur: function blur() {
close(ctx);
}
},
list: {
mousedown: function mousedown(event) {
event.preventDefault();
},
click: function click$1(event) {
click(event, ctx);
}
}
};
eventsManager(privateEvents, function (element, event) {
if (!ctx.resultsList && event !== "input") return;
if (publicEvents[element][event]) return;
publicEvents[element][event] = privateEvents[element][event];
});
eventsManager(publicEvents, function (element, event) {
ctx[element].addEventListener(event, publicEvents[element][event]);
});
};
var removeEvents = function removeEvents(ctx) {
eventsManager(ctx.events, function (element, event) {
ctx[element].removeEventListener(event, ctx.events[element][event]);
});
};
function init (ctx) {
var _this = this;
return new Promise(function ($return, $error) {
var placeHolder, resultsList, parentAttrs;
placeHolder = ctx.placeHolder;
resultsList = ctx.resultsList;
parentAttrs = {
role: "combobox",
"aria-owns": resultsList.id,
"aria-haspopup": true,
"aria-expanded": false
};
create(ctx.input, _objectSpread2(_objectSpread2({
"aria-controls": resultsList.id,
"aria-autocomplete": "both"
}, placeHolder && {
placeholder: placeHolder
}), !ctx.wrapper && _objectSpread2({}, parentAttrs)));
if (ctx.wrapper) ctx.wrapper = create("div", _objectSpread2({
around: ctx.input,
"class": ctx.name + "_wrapper"
}, parentAttrs));
if (resultsList) ctx.list = create(resultsList.tag, _objectSpread2({
dest: [resultsList.destination, resultsList.position],
id: resultsList.id,
role: "listbox",
hidden: "hidden"
}, resultsList["class"] && {
"class": resultsList["class"]
}));
addEvents(ctx);
if (ctx.data.cache) {
return getData(ctx).then(function ($await_2) {
try {
return $If_1.call(_this);
} catch ($boundEx) {
return $error($boundEx);
}
}, $error);
}
function $If_1() {
eventEmitter("init", ctx);
return $return();
}
return $If_1.call(_this);
});
}
function extend (autoComplete) {
var prototype = autoComplete.prototype;
prototype.init = function () {
init(this);
};
prototype.start = function (query) {
start(this, query);
};
prototype.unInit = function () {
if (this.wrapper) {
var parentNode = this.wrapper.parentNode;
parentNode.insertBefore(this.input, this.wrapper);
parentNode.removeChild(this.wrapper);
}
removeEvents(this);
};
prototype.open = function () {
open(this);
};
prototype.close = function () {
close(this);
};
prototype.goTo = function (index) {
goTo(index, this);
};
prototype.next = function () {
next(this);
};
prototype.previous = function () {
previous(this);
};
prototype.select = function (index) {
select(this, null, index);
};
prototype.search = function (query, record, options) {
return search(query, record, options);
};
}
function autoComplete(config) {
this.options = config;
this.id = autoComplete.instances = (autoComplete.instances || 0) + 1;
this.name = "autoComplete";
this.wrapper = 1;
this.threshold = 1;
this.debounce = 0;
this.resultsList = {
position: "afterend",
tag: "ul",
maxResults: 5
};
this.resultItem = {
tag: "li"
};
configure(this);
extend.call(this, autoComplete);
init(this);
}
return autoComplete;
})));

View File

@@ -1 +0,0 @@
<svg viewBox="0 0 1742.79 1984.21" xmlns="http://www.w3.org/2000/svg"><ellipse cx="667.97" cy="1872.93" fill="#c7c8ca" rx="649.71" ry="111.28"/><path d="m933.76 1775.81c0-29.5-23.92-53.42-53.42-53.42h-163.28c-24.37 0-45.97-15.91-52.98-39.26l-105.96-347.65 18.1-9.63c251.03-105.38 519.58 82.75 706.54-97.93l1.17-1.17c23.21-21.02 46.27-42.47 114.72.29 73.56 46.12 236 166.53 309.71 372.32 0 0 44.66-15.91 32.25-70.49-12.41-54.73-102.89-212.36-287.96-352.18-33.86-25.69-64.36-47-89.76-64.07 36.93-158.21-155.14-349.11-342.69-259.21-134.57-126.54-284.6-178.2-422.67-176.16-365.6 5.11-647.58 386.04-344.3 749.45l.58.58c4.67 5.55 9.49 11.09 14.3 16.64 19.7 23.64 32.69 44.51 43.93 81.29l90.34 296.57h-31.96c-29.48 0-53.42 23.94-53.42 53.42h121.43l204.04 96.47c12.55-26.71 1.17-58.53-25.4-71.08l-53.56-25.25h153.83c0-29.48-23.94-53.42-53.42-53.42h-139.38c-24.37 0-45.97-15.91-52.98-39.26l-71.66-235.42c-17.81-61.88 23.21-102.31 61.74-107.13 35.17-4.38 52.83 18.39 69.47 73.27l63.63 208.85h-31.96c-29.48 0-53.42 23.94-53.42 53.42h121.43l204.04 96.47c12.55-26.71 1.17-58.53-25.4-71.08l-53.56-25.25h177.88z"/><path d="m545.68 2.53h52.19v1441.85h-52.19z" fill="#f89a16" transform="matrix(.9855856 -.16917749 .16917749 .9855856 -114.16 107.17)"/><path d="m581.21 624.21-55.8-17.21-93.34-544.54 53.51 3.6z" fill="#da7c2b"/><path d="m981.62 279.44c-30.85 1.91-83.72 3.58-83.72 3.58l79.01-31.06-43.25-251.96-933.66 160.24 56.63 330 68.11 13.66s-38.47 19-60.89 28.37l22.52 131.23 933.66-160.24z" fill="#f89a16"/><circle cx="1144.16" cy="1031.93" fill="#fff" r="82.26" transform="matrix(.70710678 -.70710678 .70710678 .70710678 -394.57 1111.29)"/><circle cx="1124.15" cy="1004.52" r="52.63" transform="matrix(.41786707 -.90850818 .90850818 .41786707 -258.21 1606.05)"/><g fill="#fff"><path d="m387.43 559.95-69.59-57.58 107.75-360.2 93.69-15.45 226.75 308.17-45.64 75.04-312.97 50.02zm-11.05-75.33 25.78 21.33 266.91-42.65 15.63-25.7-187.97-255.46-31.41 5.18-88.94 297.31z"/><path d="m526.16 300.48 5.44 89.61-20.72 3.32-28.24-85.96-9.08-47.73 43.52-6.98 9.08 47.73zm18.46 103.04 7.02 36.88-41.43 6.64-7.02-36.88z"/></g></svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -1,4 +1,3 @@
/* Modified from https://github.com/TarekRaafat/autoComplete.js (version 10.2.6)*/
.autoComplete_wrapper {
display: inline-block;
position: relative;
@@ -23,9 +22,6 @@
.autoComplete_wrapper > ul {
position: absolute;
min-width: 100%;
width: fit-content;
max-width: 600px;
max-height: 226px;
overflow-y: scroll;
top: 100%;
@@ -63,11 +59,6 @@
transition: all 0.2s ease;
}
.autoComplete_wrapper > ul > li > a {
overflow: hidden;
text-overflow: ellipsis;
}
.autoComplete_wrapper > ul > li::selection {
color: rgba(#ffffff, 0);
background-color: rgba(#ffffff, 0);

View File

@@ -1,6 +0,0 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
<g id="SVGRepo_iconCarrier"> <g id="Interface / Download"> <path id="Vector" d="M6 21H18M12 3V17M12 17L17 12M12 17L7 12" stroke="#f6f5f4" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </g> </g>
</svg>

Before

Width:  |  Height:  |  Size: 546 B

View File

@@ -1,159 +0,0 @@
@font-face {
font-family:"DM Sans";
font-style: normal;
font-weight: 400;
src : url('../skin/fonts/DMSans-Regular.ttf?KIWIXCACHEID');
}
@font-face {
font-family:"DM Sans Bold";
font-style: normal;
font-weight: 700;
src : url('../skin/fonts/DMSans-Regular.ttf?KIWIXCACHEID');
}
body {
background: linear-gradient(to bottom right, #ffffff, #e6e6e6);
background-repeat: no-repeat;
background-attachment: fixed;
}
header {
width: 100%;
margin: auto;
text-align: center;
margin-top: 15%;
margin-bottom: 15%;
}
header img {
width: 60%;
min-width: 200px;
max-width: 500px;
max-height: 300px;
}
section {
display: flex;
flex-direction: column;
align-items: center;
}
header, .intro {
font-family: "DM Sans";
}
.intro {
font-size: 1em;
padding: 0 10%;
line-height: 1.2em;
text-align: center;
}
.intro h1 {
line-height: 1.1em;
font-family: "DM Sans Bold";
font-size: 1.2em;
}
.intro code {
font-family: monospace;
font-size: 1.1em;
word-break: break-all;
}
.intro a, .intro a:active, .intro a:visited {
color: #00b4e4;
text-decoration: none;
word-break: break-all;
}
.advice {
width: 80%;
margin: auto;
margin-bottom: 15%;
margin-top: 5em;
background-color: #ffffff;
border-radius: 1rem;
border: 1px solid #b7b7b7;
padding: 2em;
font-family: "DM Sans";
font-size: .9em;
box-sizing: border-box;
align-items: normal;
}
.advice p {
margin-bottom: 1em;
}
.advice p:first-child {
margin-top: 0;
}
.advice p.list-intro {
margin: 0;
}
.advice ul {
list-style-type: square;
margin: 0;
padding: 0 1em;
}
.advice ul li {
line-height: 2em;
}
.advice p:last-child {
margin-bottom: 0;
}
/* sm: 640px+ */
@media (width >= 40rem) {
header {
margin-bottom: 1em;
margin-top: 5em;
}
header img {
width: 50%;
}
.intro h1 {
font-size: 2em;
}
.advice {
width: 50%;
}
}
/* xl: 1280px+ */
@media (width >= 80rem) {
.intro h1 {
font-size: 3.4em;
}
}
/* 2xl: 1536px+ */
@media (width >= 96rem) {
header img {
width: 25%;
min-width: 200px;
max-width: 500px;
max-height: 300px;
}
.advice {
width: 25%;
min-width: 200px;
min-width: 300px;
max-width: 500px;
}
}

View File

Binary file not shown.

View File

@@ -23,8 +23,7 @@ const Translations = {
return;
const errorMsg = `Error loading translations for language '${lang}': `;
const translationJsonUrl = import.meta.resolve(`./i18n/${lang}.json`);
this.promises[lang] = fetch(translationJsonUrl).then(async (resp) => {
this.promises[lang] = fetch(`./skin/i18n/${lang}.json`).then(async (resp) => {
if ( resp.ok ) {
this.data[lang] = JSON.parse(await resp.text());
} else {
@@ -70,70 +69,30 @@ function $t(msgId, params={}) {
}
}
const I18n = {
instantiateParameterizedMessages: function(data) {
if ( data.__proto__ == Array.prototype ) {
const result = [];
for ( const x of data ) {
result.push(this.instantiateParameterizedMessages(x));
}
return result;
} else if ( data.__proto__ == Object.prototype ) {
const msgId = data.msgid;
const msgParams = data.params;
if ( msgId && msgId.__proto__ == String.prototype && msgParams && msgParams.__proto__ == Object.prototype ) {
return $t(msgId, msgParams);
} else {
const result = {};
for ( const p in data ) {
result[p] = this.instantiateParameterizedMessages(data[p]);
function getCookie(cookieName) {
const name = cookieName + "=";
let result;
decodeURIComponent(document.cookie).split('; ').forEach(val => {
if (val.indexOf(name) === 0) {
result = val.substring(name.length);
}
return result;
}
} else {
return data;
}
},
render: function (template, params) {
params = this.instantiateParameterizedMessages(params);
return mustache.render(template, params);
}
});
return result;
}
const DEFAULT_UI_LANGUAGE = 'en';
Translations.load(DEFAULT_UI_LANGUAGE, /*asDefault=*/true);
// Below function selects the most suitable UI language from the list
// of preferred languages in browser preferences and available translations.
// Since, unlike Accept-Language header, navigator.languages doesn't contain
// qvalues, they are computed using the same algorithm as in Firefox 121
function getDefaultUserLanguage() {
const mostSuitableLang = { code: DEFAULT_UI_LANGUAGE, score: 0 }
const n = navigator.languages.length;
for (const lang of uiLanguages ) {
const rank = navigator.languages.indexOf(lang.iso_code);
if ( rank >= 0 ) {
const qvalue = Math.round(10*(1 - rank/n))/10;
const score = qvalue * lang.translation_count;
if ( score > mostSuitableLang.score ) {
mostSuitableLang.code = lang.iso_code;
mostSuitableLang.score = score;
}
}
}
return mostSuitableLang.code;
}
function getUserLanguage() {
return new URLSearchParams(window.location.search).get('userlang')
|| window.localStorage.getItem('userlang')
|| getDefaultUserLanguage();
|| getCookie('userlang')
|| DEFAULT_UI_LANGUAGE;
}
function setUserLanguage(lang, callback) {
window.localStorage.setItem('userlang', lang);
setPermanentGlobalCookie('userlang', lang);
Translations.load(lang);
Translations.whenReady(callback);
}
@@ -185,46 +144,15 @@ function initUILanguageSelector(activeLanguage, languageChangeCallback) {
}
const languageSelector = document.getElementById("ui_language");
for (const lang of uiLanguages ) {
const is_selected = lang.iso_code == activeLanguage;
languageSelector.appendChild(new Option(lang.self_name, lang.iso_code, is_selected, is_selected));
const lang_name = Object.getOwnPropertyNames(lang)[0];
const lang_code = lang[lang_name];
const is_selected = lang_code == activeLanguage;
languageSelector.appendChild(new Option(lang_name, lang_code, is_selected, is_selected));
}
languageSelector.onchange = languageChangeCallback;
}
function parseDom(html) {
const domParser = new DOMParser();
return domParser.parseFromString(html, "text/html").documentElement;
}
function translatePageInWindow(w) {
if ( w.KIWIX_RESPONSE_TEMPLATE && w.KIWIX_RESPONSE_DATA ) {
const template = parseDom(w.KIWIX_RESPONSE_TEMPLATE).textContent;
// w.KIWIX_RESPONSE_DATA may belong to a different context and running
// I18n.render() on it directly won't work correctly
// because the type checks (obj.__proto__ == ???.prototype) in
// I18n.instantiateParameterizedMessages() will fail (String.prototype
// refers to different objects in different contexts).
// Work arround that issue by copying the object into our context.
const params = JSON.parse(JSON.stringify(w.KIWIX_RESPONSE_DATA));
const newHtml = I18n.render(template, params);
w.document.documentElement.innerHTML = parseDom(newHtml).innerHTML;
}
}
function translateSelf() {
if ( window.KIWIX_RESPONSE_TEMPLATE && window.KIWIX_RESPONSE_DATA ) {
setUserLanguage(getUserLanguage(), () => {
translatePageInWindow(window)
});
}
};
window.$t = $t;
window.getUserLanguage = getUserLanguage;
window.setUserLanguage = setUserLanguage;
window.initUILanguageSelector = initUILanguageSelector;
window.translatePageInWindow = translatePageInWindow;
window.I18n = I18n;
window.addEventListener('load', translateSelf);

View File

@@ -2,9 +2,6 @@
"@metadata": {
"authors": [
"Asma",
"Hamoudak",
"Meno25",
"Mohammed Qays",
"Ravan",
"محمد أحمد عبد الفتاح"
]
@@ -15,7 +12,7 @@
"no-book-found": "لا يوجد كتاب يطابق معايير الاختيار",
"url-not-found": "لم يتم العثور على عنوان URL المطلوب \"{{url}}\" على هذا الخادم.",
"suggest-search": "قم بإجراء بحث عن النص الكامل لـ <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
"random-article-failure": "للأسف ! فشل في اختيار مقال عشوائي :(",
"random-article-failure": "مع الأسف! فشل اختيار مقال عشوائي :(",
"invalid-raw-data-type": "{{DATATYPE}} ليس طلبًا صالحًا للمحتوى الأولي.",
"no-value-for-arg": "لم يتم تقديم قيمة للوسيطة {{ARGUMENT}}",
"no-query": "لم يتم تقديم ملخص.",
@@ -24,32 +21,13 @@
"400-page-heading": "طلب غير صالح",
"404-page-title": "المحتوى غير موجود",
"404-page-heading": "لم يتم العثور عليه",
"500-page-title": "خطأ داخلي بالخادم",
"500-page-heading": "عفواً، الصفحة لا تعمل.",
"500-page-text": "لا يمكن إرسال المسار المطلوب بشكل صحيح:",
"fulltext-search-unavailable": "البحث في كامل النص غير متاح",
"no-search-results": "محرك البحث في كامل النص غير متوفر لهذا المحتوي.",
"library-button-text": "توجه إلي صفحة الترحيب",
"home-button-text": "انتقل إلى الصفحة الرئيسة لـ '{{{BOOK_TITLE}}}'",
"500-page-title": "خطأ في الخادم الداخلي",
"500-page-heading": "خطأ في الخادم الداخلي",
"fulltext-search-unavailable": "البحث عن النص الكامل غير متاح",
"no-search-results": "محرك البحث عن النص الكامل غير متاح لهذا المحتوى.",
"library-button-text": "اذهب لصفحة الترحيب",
"home-button-text": "انتقل إلى الصفحة الرئيسية لـ \"{{BOOK_TITLE}}\"",
"random-page-button-text": "اذهب إلى صفحة عشوائية",
"searchbox-tooltip": "ابحث عن '{{{BOOK_TITLE}}}'",
"confusion-of-tongues": "قد يشارك في البحث كتابان أو أكثر بلغات مختلفة، مما قد يؤدي إلى نتائج محيرة.",
"direct-download-alt-text": "التنزيل مباشرة عبر بروتوكول HTTP(S)",
"hash-download-link-text": "المجموع الإختباري لخوارزمية SHA-256",
"hash-download-alt-text": "عرض المجموع الإختباري لخوارزمية SHA-256 للملف",
"magnet-link-text": "رابط جاذب",
"magnet-alt-text": "التنزيل بواسطة الرابط الجاذب",
"torrent-download-link-text": "بت تورنت",
"torrent-download-alt-text": "التنزيل بواسطة بت تورنت",
"unknown-error": "خطأ غير معروف",
"book-category.wikibooks": "ويكي الكتب",
"book-category.wikinews": "ويكي الأخبار",
"book-category.wikipedia": "ويكيبيديا",
"book-category.wikiquote": "ويكي الإقتباس",
"book-category.wikisource": "ويكي مصدر",
"book-category.wikispecies": "ويكي أنواع",
"book-category.wikiversity": "ويكي الجامعة",
"book-category.wikivoyage": "ويكي الرحلات",
"book-category.wiktionary": "ويكي القاموس",
"book-category.other": "أخرى {كتب أخري}"
"searchbox-tooltip": "بحث \"{{BOOK_TITLE}}\"",
"confusion-of-tongues": "قد يشارك في البحث كتابان أو أكثر بلغات مختلفة، مما قد يؤدي إلى نتائج محيرة."
}

View File

@@ -1,41 +1,18 @@
{
"@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": "স্বাগত পাতায় চলুন",
"home-button-text": "'{{BOOK_TITLE}}'-এর প্রধান পাতায় চলুন",
"searchbox-tooltip": "'{{BOOK_TITLE}}' অনুসন্ধান করুন",
"search": "অনুসন্ধান",
"welcome-to-kiwix-server": "কিউইক্স সার্ভারে স্বাগতম",
"download-links-title": "বই ডাউনলোড করুন",
"preview-book": "প্রাকদর্শন",
"book-category.wikibooks": "উইকিবই",
"book-category.wikinews": "উইকিসংবাদ",
"book-category.wikipedia": "উইকিপিডিয়া",
"book-category.wikiquote": "উইকিউক্তি",
"book-category.wikisource": "উইকিসংকলন",
"book-category.wikispecies": "উইকিপ্রজাতি",
"book-category.wikiversity": "উইকিবিশ্ববিদ্যালয়",
"book-category.wikivoyage": "উইকিভ্রমণ",
"book-category.wiktionary": "উইকিঅভিধান",
"book-category.other": "অন্যান্য"
"preview-book": "প্রাকদর্শন"
}

View File

@@ -1,42 +0,0 @@
{
"@metadata": {
"authors": [
"Adriendelucca",
"Y-M D"
]
},
"name": "brezhoneg",
"suggest-full-text-search": "E lec'h emañ \"{{{SEARCH_TERMS}}}\"...",
"no-such-book": "Neus ket eus al levr-mañ: {{BOOK_NAME}}",
"no-book-found": "Neus levr ebet a glot gant an dezverkoù-se",
"url-not-found": "Neo ket bet kavet an URL \"{{url}}\" goulennet war ar servijer-mañ.",
"random-article-failure": "Chaous! Nhon eus ket gellet dibab ur pennad dre ziouer evidoch :(",
"400-page-title": "Reked amwiriek",
"400-page-heading": "Reked amwiriek",
"404-page-heading": "N'eo ket bet kavet",
"500-page-title": "Fazi diabarzh ar servijer",
"500-page-heading": "Fazi diabarzh ar servijer",
"search-results-page-title": "Klask: {{SEARCH_PATTERN}}",
"search-results-page-header": "Disochoù <b>{{START}}-{{END}}</b> diwar <b>{{COUNT}}</b> evit <b>\"{{{SEARCH_PATTERN}}}\"</b>",
"empty-search-results-page-header": "Disoch ebet kavet evit <b>\"{{{SEARCH_PATTERN}}}\"</b>",
"search-result-book-info": "diouzh {{BOOK_TITLE}}",
"word-count": "{{COUNT}} a cherioù",
"library-button-text": "Mont dar bajenn degemer",
"home-button-text": "Mont da bajenn degemer \"{{BOOK_TITLE}}\"",
"random-page-button-text": "Mont dur bajenn dre zegouezh",
"searchbox-tooltip": "Klask '{{BOOK_TITLE}}'",
"powered-by-kiwix-html": "Lusket gant&nbsp;<a href=\"https://kiwix.org\">Kiwix</a>",
"search": "Klask",
"book-filtering-all-categories": "An holl rummadoù",
"book-filtering-all-languages": "An holl yezhoù",
"count-of-matching-books": "{{COUNT}} levr",
"download": "Pellgargañ",
"direct-download-link-text": "Eeun",
"filter-by-tag": "Silañ gant an dikedenn \"{{TAG}}\"",
"stop-filtering-by-tag": "Paouez da silañ gant an dikedenn \"{{TAG}}\"",
"welcome-to-kiwix-server": "Degemer mat er servijer Kiwix",
"download-links-heading": "Liammoù pellgargañ evit <b><i>{{BOOK_TITLE}}</i></b>",
"download-links-title": "Pellgargañ al levr",
"preview-book": "Rakwelet",
"unknown-error": "Fazi dianav"
}

View File

@@ -1,55 +0,0 @@
{
"@metadata": {
"authors": [
"Kalakpagh",
"Ruky Wunpini"
]
},
"name": "Silimiinsili",
"suggest-full-text-search": "Gbubi la '{{{SEARCH_TERMS}}}'...",
"no-such-book": "Lala buku kani:{{BOOK_NAME}}",
"too-many-books": "Buku nima pam ka bɛ daa suhi ({{NB_BOOKS}}) din ni ka tariga nyɛ {{LIMIT}}",
"no-book-found": "Buku kani lu zahim a ni piigi yaɣa shɛli",
"url-not-found": "URL \"{{url}}\" shɛli bɛ ni daa suhi daa kani n-ti tum tumda ŋɔ.",
"suggest-search": "Niŋmi lahabali pali vihigu zaŋ n-ti <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
"random-article-failure": "Oops! Zaɣisiya ni di gahim piigi lahabali :(",
"invalid-raw-data-type": "{{DATATYPE}} nyɛla din suhibu bi niŋ viɛnyɛla zaŋ n-ti lahabali kahili.",
"invalid-request": "URL \"{{{url}}}\" shɛli bɛ ni daa suhi ŋɔ nyɛla din bi suhi viɛnyɛla.",
"no-value-for-arg": "Dariza shɛli bi ti zaŋ n-ti nangban'kpeeni {{ARGUMENT}}",
"no-query": "Yɛlshɛli bi yiina",
"raw-entry-not-found": "Ku tooi nya {{DATATYPE}} kpɛbu {{ENTRY}}",
"400-page-title": "Suhigu din bi niŋ viɛnyɛla",
"400-page-heading": "Suhigu din bi niŋ viɛnyɛla",
"404-page-title": "Lahabali kani",
"404-page-heading": "Kani",
"500-page-title": "Puuni tum tumda chiriŋ",
"500-page-heading": "Puuni tum tumda chiriŋ",
"500-page-text": "Puuni tum tumda chiriŋ niŋya. Ti niŋ yolitem zaŋ jɛndi li :/",
"fulltext-search-unavailable": "Lahabali pali vihigu kani",
"search-results-page-title": "Vihima:{{SEARCH_PATTERN}}",
"search-results-page-header": "Chaɣili nima <b>{{START}}-{{END}}</b> of <b>{{COUNT}}</b> for <b>\"{{{SEARCH_PATTERN}}}\"</b>",
"empty-search-results-page-header": "Chaɣili daa kani zaŋ n-ti\n <b>\"{{{SEARCH_PATTERN}}}\"</b>",
"search-result-book-info": "yina {{BOOK_TITLE}}",
"word-count": "{{COUNT}} bachi nima",
"library-button-text": "Cham solɔɣu",
"home-button-text": "Cham yaɣili maŋmaŋ zaŋ n-ti\n'{{BOOK_TITLE}}'",
"random-page-button-text": "Cham gahim piigi yaɣili",
"searchbox-tooltip": "Vihima '{{BOOK_TITLE}}'",
"confusion-of-tongues": "Buku nima ayi bee gari balli koŋkoba nyɛ din yɛn be vihigu ŋɔ ni ka di ni tooi chɛ ka di laasabu wali.",
"welcome-page-overzealous-filter": "Labisibu kani. A ni yu ni a\n<a href=\"{{URL}}\">reset filter</a>?",
"powered-by-kiwix-html": "Din niŋ li nyɛ &nbsp;<a href=\"https://kiwix.org\">Kiwix</a>",
"search": "Vihima",
"book-filtering-all-categories": "Pubu zaa",
"book-filtering-all-languages": "Bala zaa",
"count-of-matching-books": "{{COUNT}} Buku(nima)",
"download": "Deebu",
"direct-download-link-text": "Tibi",
"direct-download-alt-text": "Tibi deebu",
"hash-download-link-text": "Sha256 hash",
"hash-download-alt-text": "Deebu daliŋ",
"welcome-to-kiwix-server": "Maraba Kiwix tum tumda",
"download-links-heading": "Deemi soli zaŋ n-ti <b><i>{{BOOK_TITLE}}</i></b>",
"download-links-title": "Deemi buku",
"preview-book": "Daŋyuli",
"unknown-error": "Chiriŋ din bi tooi baŋ"
}

View File

@@ -1,79 +1,20 @@
{
"@metadata": {
"authors": [
"AnnaTheProgrammer777",
"IMayBeABitShy",
"Justman10000",
"Lucas Werkmeister",
"MoritzMT20",
"Rofiatmustapha12",
"ThisCarthing"
]
},
"name": "Deutsch",
"suggest-full-text-search": "enthält '{{{SEARCH_TERMS}}}'...",
"no-such-book": "Buch nicht gefunden: {{BOOK_NAME}}",
"too-many-books": "Zu viele Bücher angefragt ({{NB_BOOKS}}), die Beschränkung liegt bei {{LIMIT}}",
"no-book-found": "Keine Bücher entsprechen den Auswahlkriterien",
"url-not-found": "Die angeforderte URL \"{{url}}\" konnte auf diesem Server nicht gefunden werden.",
"suggest-search": "Führe eine Volltextsuche nach <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a> durch",
"random-article-failure": "Hoppla! Konnte keinen zufälligen Artikel auswählen :(",
"invalid-raw-data-type": "{{DATATYPE}} ist keine gültige Anfrage für unverarbeiteten Inhalt",
"invalid-request": "Die angeforderte URL „{{{url}}}“ ist keine gültige Anfrage.",
"no-value-for-arg": "Kein Wert für den Parameter {{ARGUMENT}} gegeben",
"no-query": "Keine Suchanfrage gegeben.",
"raw-entry-not-found": "Eintrag {{ENTRY}} des Typs {{DATATYPE}} konnte nicht gefunden werden.",
"400-page-title": "Ungültige Anfrage",
"400-page-heading": "Ungültige Anfrage",
"404-page-title": "Inhalt nicht gefunden",
"404-page-heading": "Nicht gefunden",
"500-page-title": "Interner Server-Fehler",
"500-page-heading": "Interner Server-Fehler",
"500-page-text": "Es ist ein interner Serverfehler aufgetreten. Das tut uns leid :/",
"fulltext-search-unavailable": "Die Volltestsuche steht nicht zur Verfügung.",
"no-search-results": "Die Volltextsuche ist für diesen Inhalt nicht verfügbar.",
"search-results-page-title": "Suche: {{SEARCH_PATTERN}}",
"search-results-page-header": "Ergebnisse <b>{{START}}-{{END}}</b> von <b>{{COUNT}}</b> für <b>„{{{SEARCH_PATTERN}}}“</b>",
"empty-search-results-page-header": "Für <b>„{{{SEARCH_PATTERN}}}“</b> wurden keine Ergebnisse gefunden.",
"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",
"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>",
"search": "Suchen",
"book-filtering-all-categories": "Alle Kategorien",
"book-filtering-all-languages": "Alle Sprachen",
"count-of-matching-books": "{{COUNT}} Bücher",
"download": "Herunterladen",
"direct-download-link-text": "Direkt",
"direct-download-alt-text": "Direktes Herunterladen über HTTP(S)",
"hash-download-link-text": "SHA-256 Prüfsumme",
"hash-download-alt-text": "SHA-256-Dateiprüfsumme anzeigen",
"magnet-link-text": "Magnet Link",
"magnet-alt-text": "Download über Magnet-Link",
"torrent-download-link-text": "BitTorrent",
"torrent-download-alt-text": "Herunterladen über BitTorrent",
"library-opds-feed-all-entries": "ODPS Feed der Bibliothek - Alle Einträge",
"filter-by-tag": "Nach Tag \"{{{TAG}}}\" filtern",
"stop-filtering-by-tag": "Filterung nach Tag \"{{{TAG}}}\" aufheben",
"library-opds-feed-parameterised": "ODPS Feed der Bibliothek - Einträge mit {{#LANG}\nSprache {{LANG}} {{/LANG}}{{#CATEGORY}}\nKategorie: {{CATEGORY}} {{/CATEGORY}}{{#TAG}}\nTag: {{TAG}}{{/TAG}}{{#Q}}\nQuery: {{Q}} {{/Q}}",
"welcome-to-kiwix-server": "Wilkommen beim Kiwix Server",
"download-links-heading": "Download Links für <b><i>{{BOOK_TITLE}}</i></b>",
"download-links-title": "Buch herunterladen",
"preview-book": "Vorschau",
"unknown-error": "Unbekannter Fehler",
"book-category.wikibooks": "Wikibooks",
"book-category.wikinews": "Wikinews",
"book-category.wikipedia": "Wikipedia",
"book-category.wikiquote": "Wikiquote",
"book-category.wikisource": "Wikisource",
"book-category.wikispecies": "Wikispecies",
"book-category.wikiversity": "Wikiversity",
"book-category.wikivoyage": "Wikivoyage",
"book-category.wiktionary": "Wiktionary",
"book-category.other": "Andere"
"searchbox-tooltip": "Nach '{{BOOK_TITLE}}' suchen"
}

View File

@@ -1,7 +1,8 @@
{
"@metadata": {
"authors": [
"Alhaji Yakubu"
"Alhaji Yakubu",
"Amire80"
]
},
"welcome-page-overzealous-filter": "Duoro kyebe. <a href=\"{{URL}}\">E na boɔra ka fo</a>?",

View File

@@ -1,18 +1,10 @@
{
"@metadata": {
"authors": [
"Jimkats",
"Kelson",
"Norhorn",
"Ανώνυμος Βικιπαιδιστής"
"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": "Αναζήτηση",
@@ -23,14 +15,8 @@
"direct-download-link-text": "Απευθείας",
"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}}}\"",
"welcome-to-kiwix-server": "Καλώς ορίσατε στον διακομιστή Kiwix",
"download-links-heading": "Λήψη συνδέσμων για <b><i>{{BOOK_TITLE}}</i></b>",
"download-links-title": "Κατεβάστε το βιβλίο",
"preview-book": "Προεπισκόπηση",
"unknown-error": "Άγνωστο σφάλμα"
"torrent-download-link-text": "Αρχείο torrent",
"torrent-download-alt-text": "λήψη torrent",
"filter-by-tag": "Φίλτρο ανά ετικέτα \"{{TAG}}\"",
"stop-filtering-by-tag": "Διακοπή φίλτρου ανά ετικέτα \"{{TAG}}\""
}

View File

@@ -12,7 +12,6 @@
, "suggest-search" : "Make a full text search for <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>"
, "random-article-failure" : "Oops! Failed to pick a random article :("
, "invalid-raw-data-type" : "{{DATATYPE}} is not a valid request for raw content."
, "invalid-request" : "The requested URL \"{{{url}}}\" is not a valid request."
, "no-value-for-arg": "No value provided for argument {{ARGUMENT}}"
, "no-query" : "No query provided."
, "raw-entry-not-found" : "Cannot find {{DATATYPE}} entry {{ENTRY}}"
@@ -20,36 +19,14 @@
, "400-page-heading" : "Invalid request"
, "404-page-title" : "Content not found"
, "404-page-heading" : "Not Found"
, "new-404-page-title" : "Page not found"
, "new-404-page-heading" : "Oops. Page not found."
, "404-img-text": "Not found!"
, "path-was-not-found": "The requested path was not found:"
, "404-advice.p1": "The content you're looking for may still be available, but it might be located at a different place within the ZIM file."
, "404-advice.p2": "Please:"
, "404-advice.p3": "Try using the search function to find the content you want"
, "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"
, "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"
, "external-link-advice.p1": "The link you're trying to access is not part of your offline package and requires an internet connection."
, "external-link-advice.p2": "If you can go online, you can attempt to open the link."
, "external-link-advice.p3": "You can otherwise return to your ZIM's offline content by using your browser's back button."
, "500-page-heading" : "Internal Server Error"
, "fulltext-search-unavailable" : "Fulltext search unavailable"
, "no-search-results": "The fulltext search engine is not available for this content."
, "search-results-page-title": "Search: {{SEARCH_PATTERN}}"
, "search-results-page-header": "Results <b>{{START}}-{{END}}</b> of <b>{{COUNT}}</b> for <b>\"{{{SEARCH_PATTERN}}}\"</b>"
, "empty-search-results-page-header": "No results were found for <b>\"{{{SEARCH_PATTERN}}}\"</b>"
, "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>"
@@ -59,40 +36,19 @@
, "count-of-matching-books": "{{COUNT}} book(s)"
, "download": "Download"
, "direct-download-link-text": "Direct"
, "direct-download-alt-text": "Download directly via HTTP(S)"
, "hash-download-link-text": "SHA-256 checksum"
, "hash-download-alt-text": "Display SHA-256 file checksum"
, "direct-download-alt-text": "direct download"
, "hash-download-link-text": "Sha256 hash"
, "hash-download-alt-text": "download hash"
, "magnet-link-text": "Magnet link"
, "magnet-alt-text": "Download via Magnet link"
, "torrent-download-link-text": "BitTorrent"
, "torrent-download-alt-text": "Download via BitTorrent"
, "magnet-alt-text": "download magnet"
, "torrent-download-link-text": "Torrent file"
, "torrent-download-alt-text": "download torrent"
, "library-opds-feed-all-entries": "Library OPDS Feed - All entries"
, "filter-by-tag": "Filter by tag \"{{{TAG}}}\""
, "stop-filtering-by-tag": "Stop filtering by tag \"{{{TAG}}}\""
, "filter-by-tag": "Filter by tag \"{{TAG}}\""
, "stop-filtering-by-tag": "Stop filtering by tag \"{{TAG}}\""
, "library-opds-feed-parameterised": "Library OPDS Feed - entries matching {{#LANG}}\nLanguage: {{LANG}} {{/LANG}}{{#CATEGORY}}\nCategory: {{CATEGORY}} {{/CATEGORY}}{{#TAG}}\nTag: {{TAG}} {{/TAG}}{{#Q}}\nQuery: {{Q}} {{/Q}}"
, "welcome-to-kiwix-server": "Welcome to Kiwix Server"
, "download-links-heading": "Download links for <b><i>{{BOOK_TITLE}}</i></b>"
, "download-links-title": "Download book"
, "preview-book": "Preview"
, "non-translated-text": "{{MSG}}"
, "unknown-error": "Unknown error"
, "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": "Wikibooks"
, "book-category.wikihow": "wikiHow"
, "book-category.wikinews": "Wikinews"
, "book-category.wikipedia": "Wikipedia"
, "book-category.wikiquote": "Wikiquote"
, "book-category.wikisource": "Wikisource"
, "book-category.wikispecies": "Wikispecies"
, "book-category.wikiversity": "Wikiversity"
, "book-category.wikivoyage": "Wikivoyage"
, "book-category.wiktionary": "Wiktionary"
, "book-category.other": "Other"
, "text-loading-content": "Loading Content"
}

View File

@@ -1,104 +0,0 @@
{
"@metadata": {
"authors": [
"AlexanderFF",
"Fitoschido",
"Laranxa",
"Ovruni",
"Sinopsistrans",
"SpikeShroom",
"Vis4valentine"
]
},
"name": "Español",
"suggest-full-text-search": "que contenga \"{{{SEARCH_TERMS}}}\"...",
"no-such-book": "No existe el libro: {{BOOK_NAME}}",
"too-many-books": "Demasiadas solicitudes de libros ({{NB_BOOKS}}) donde el límite es {{LIMIT}}",
"no-book-found": "Ningún libro coincide con los criterios de selección",
"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 :(",
"invalid-raw-data-type": "{{DATATYPE}} no es una solicitud válida de contenido en crudo.",
"invalid-request": "La URL solicitada \"{{{url}}}\" no es una solicitud válida.",
"no-value-for-arg": "No se ha proporcionado ningún valor para el argumento {{ARGUMENT}}",
"no-query": "No se ha proporcionado ninguna consulta.",
"raw-entry-not-found": "No se puede encontrar la entrada {{DATATYPE}} {{ENTRY}}",
"400-page-title": "Solicitud inválida",
"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.",
"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}}",
"search-results-page-header": "Resultados <b>{{START}}-{{END}}</b> de <b>{{COUNT}}</b> para <b>\"{{{SEARCH_PATTERN}}}\"</b>",
"empty-search-results-page-header": "No se encontraron resultados para <b>\"{{{SEARCH_PATTERN}}}\"</b>",
"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}}}'",
"random-page-button-text": "Ir a una página seleccionada al azar",
"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>",
"search": "Buscar",
"book-filtering-all-categories": "Todas las categorías",
"book-filtering-all-languages": "Todos los idiomas",
"count-of-matching-books": "{{COUNT}} libro(s)",
"download": "Descargar",
"direct-download-link-text": "Directamente",
"direct-download-alt-text": "Descargar directamente vía HTTP(S)",
"hash-download-link-text": "Suma de verificación SHA-256",
"hash-download-alt-text": "Mostrar la suma de verificación de archivos SHA-256",
"magnet-link-text": "Enlace magnético",
"magnet-alt-text": "Descargar mediante enlace Magnet",
"torrent-download-link-text": "BitTorrent",
"torrent-download-alt-text": "Descargar a través de BitTorrent",
"library-opds-feed-all-entries": "Biblioteca OPDS Feed - Todas las entradas",
"filter-by-tag": "Filtrar por etiqueta \"{{{TAG}}}\"",
"stop-filtering-by-tag": "Dejar de filtrar por etiqueta \"{{{TAG}}}\"",
"library-opds-feed-parameterised": "Feed OPDS de la biblioteca: entradas que coinciden con {{#LANG}}\nLanguage: {{LANG}} {{/LANG}}{{#CATEGORY}}\nCategory: {{CATEGORY}} {{/CATEGORY}} {{#TAG}}\nEtiqueta: {{TAG}} {{/TAG}}{{#Q}}\nConsulta: {{Q}} {{/Q}}",
"welcome-to-kiwix-server": "Bienvenido al servidor Kiwix",
"download-links-heading": "Enlaces de descarga para <b><i>{{BOOK_TITLE}}</i></b>",
"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",
"book-category.wikisource": "Wikisource",
"book-category.wikispecies": "Wikiespecies",
"book-category.wikiversity": "Wikiversidad",
"book-category.wikivoyage": "Wikiviajes",
"book-category.wiktionary": "Wikcionario",
"book-category.other": "Otros",
"text-loading-content": "Cargando contenido"
}

View File

@@ -3,56 +3,28 @@
"authors": [
"MITO",
"Nike",
"Pyscowicz",
"Samoasambia"
"Pyscowicz"
]
},
"name": "suomi",
"suggest-full-text-search": "sisältää '{{{SEARCH_TERMS}}}'...",
"no-such-book": "Kirjaa {{BOOK_NAME}} ei ole olemassa",
"url-not-found": "Pyydettyä URL-osoitetta \"{{url}}\" ei löytynyt tältä palvelimelta.",
"400-page-title": "Virheellinen pyyntö",
"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}}",
"word-count": "{{COUNT}} sanaa",
"500-page-heading": "Sisäinen palvelinvirhe",
"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",
"count-of-matching-books": "{{COUNT}} kirja(a)",
"download": "Lataa",
"magnet-link-text": "Magnet-linkki",
"magnet-alt-text": "lataa magnet",
"torrent-download-link-text": "BitTorrent",
"torrent-download-alt-text": "lataa torrent-tiedosto",
"torrent-download-link-text": "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öä"
"preview-book": "Esikatsele"
}

View File

@@ -1,21 +1,12 @@
{
"@metadata": {
"authors": [
"Adriendelucca",
"Benoit74",
"Derugon",
"Gomoko",
"Goombiis",
"Melimeli",
"Stephane",
"Thibaut120094",
"Urhixidur",
"Verdy p",
"Vikoula5",
"Wladek92"
"Verdy p"
]
},
"name": "Français",
"name": "français",
"suggest-full-text-search": "contenant « {{{SEARCH_TERMS}}} »...",
"no-such-book": "Aucun livre avec ce nom: {{BOOK_NAME}}",
"too-many-books": "Trop de livres demandés ({{NB_BOOKS}}) alors que la limite est de {{LIMIT}}",
@@ -24,7 +15,6 @@
"suggest-search": "Faire une recherche en texte intégral de « <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a> »",
"random-article-failure": "Oups! Échec de sélection dun article aléatoire :(",
"invalid-raw-data-type": "{{DATATYPE}} nest pas une requête valide pour du contenu brut.",
"invalid-request": "L'URL demandée \"{{{url}}}\" n'est pas une requête valide.",
"no-value-for-arg": "Aucune valeur fournie pour largument {{ARGUMENT}}",
"no-query": "Aucune requête fournie.",
"raw-entry-not-found": "Impossible de trouver lentrée « {{ENTRY}} » de type « {{DATATYPE}} »",
@@ -32,36 +22,14 @@
"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",
"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}}",
"search-results-page-header": "Résultats <b>{{START}}-{{END}}</b> sur<b> {{COUNT}}</b> pour <b>\"{{{SEARCH_PATTERN}}}\"</b>",
"empty-search-results-page-header": "Aucun résultat na été trouvé pour <b>« {{{SEARCH_PATTERN}}} »</b>",
"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>",
@@ -71,31 +39,19 @@
"count-of-matching-books": "{{COUNT}} livre(s)",
"download": "Télécharger",
"direct-download-link-text": "Direct",
"direct-download-alt-text": "Télécharger directement via HTTP(S)",
"hash-download-link-text": "Hachage SHA-256",
"hash-download-alt-text": "Affiche le hachage SHA-256 du fichier",
"direct-download-alt-text": "téléchargement direct",
"hash-download-link-text": "Hachage sha256",
"hash-download-alt-text": "télécharger le hachage",
"magnet-link-text": "Lien Magnet",
"magnet-alt-text": "Télécharger via le lien Magnet",
"torrent-download-link-text": "BitTorrent",
"torrent-download-alt-text": "Télécharger via BitTorrent",
"magnet-alt-text": "télécharger le lien Magnet",
"torrent-download-link-text": "Fichier torrent",
"torrent-download-alt-text": "télécharger le torrent",
"library-opds-feed-all-entries": "Flux OPDS de la bibliothèque Toutes les entrées",
"filter-by-tag": "Filtrer par le tag \"{{{TAG}}}\"",
"stop-filtering-by-tag": "Arrêter de filtrer par le tag \"{{{TAG}}}\"",
"filter-by-tag": "Filtrer par la balise « {{TAG}} »",
"stop-filtering-by-tag": "Arrêter le filtrage par la balise « {{TAG}} »",
"library-opds-feed-parameterised": "Flux OPDS de la bibliothèque Entrées correspondant à {{#LANG}}:\n ▪ Langue: {{LANG}} {{/LANG}}{{#CATEGORY}}\n ▪ Catégorie: {{CATEGORY}} {{/CATEGORY}}{{#TAG}}\n ▪ Étiquette: {{TAG}} {{/TAG}}{{#Q}}\n ▪ Requête: {{Q}} {{/Q}}",
"welcome-to-kiwix-server": "Bienvenue sur le Serveur Kiwix",
"download-links-heading": "Liens de téléchargement pour <b><i>{{BOOK_TITLE}}</i></b>",
"download-links-title": "Télécharger le livre",
"preview-book": "Aperçu",
"unknown-error": "Erreur inconnue",
"book-category.wikibooks": "Wikilivres",
"book-category.wikinews": "Wikinews",
"book-category.wikipedia": "Wikipédia",
"book-category.wikiquote": "Wikiquote",
"book-category.wikisource": "Wikisource",
"book-category.wikispecies": "Wikispecies",
"book-category.wikiversity": "Wikiversité",
"book-category.wikivoyage": "Wikivoyage",
"book-category.wiktionary": "Wiktionnaire",
"book-category.other": "Autre",
"text-loading-content": "Chargement du contenu"
"preview-book": "Aperçu"
}

View File

@@ -1,67 +0,0 @@
{
"@metadata": {
"authors": [
"Abelidokoo",
"El-hussain14",
"Rofiatmustapha12",
"Smshika",
"Yusuf Sa'adu"
]
},
"name": "Turanci",
"suggest-full-text-search": "dauke da ''{{{SEARCH_TERMS}}}''...",
"no-such-book": "Babu irin wannan littafin: {{BOOK_NAME}}",
"too-many-books": "An nemi littattafai da yawa ({{NB_BOOKS}}) inda iyaka shine {{LIMIT}}",
"no-book-found": "Babu wani littafi da ya dace da ma'aunin zaɓi",
"url-not-found": "Ba a sami URL ɗin da ake nema \"{{url}}\" akan wannan sabar ba.",
"suggest-search": "Yi cikakken bincike na rubutu don <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
"random-article-failure": "Kash! An kasa ɗaukar labarin bazuwar :(",
"invalid-raw-data-type": "{{DATATYPE}} ba ingantaccen buƙatun ɗanyen abun ciki bane.",
"invalid-request": "URL ɗin da ake nema \"{{{url}}}\" ba buƙatu mai inganci bane.",
"no-value-for-arg": "Babu ƙima da aka bayar don hujja {{ARGUMENT}}",
"no-query": "Ba a bayar da tambaya ba.",
"raw-entry-not-found": "Ba a iya samun shigarwar {{DATATYPE}} {{ENTRY}}",
"400-page-title": "nema mara inganci",
"400-page-heading": "nema mara inganci",
"404-page-title": "Ba a samo abun ciki ba",
"404-page-heading": "Ba a Samu ba",
"500-page-title": "Kuskuren na Cikin Saba",
"500-page-heading": "Kuskuren na Cikin Saba",
"500-page-text": "An sami kuskuren uwar garken ciki. Munyi nadama akan hakan :/",
"fulltext-search-unavailable": "Babu binciken cikakken rubutu",
"no-search-results": "Babu injin binciken cikakken rubutu don wannan abun ciki.",
"search-results-page-title": "Bincika: {{SEARCH_PATTERN}}",
"search-results-page-header": "Sakamako <b>{{START}}-{{END}}</b> na <b>{{COUNT}}</b> na <b>\"{{{SEARCH_PATTERN}}}\"</b>",
"empty-search-results-page-header": "Ba a sami sakamakon <b>\"{{{SEARCH_PATTERN}}}\"</b>",
"search-result-book-info": "daga {{BOOK_TITLE}}",
"word-count": "{{COUNT}} kalmomi",
"library-button-text": "Je zuwa shafin maraba",
"home-button-text": "Jeka babban shafin '{{BOOK_TITLE}}'",
"random-page-button-text": "Je zuwa shafin da aka zaɓa ba da gangan ba",
"searchbox-tooltip": "Bincika '{{BOOK_TITLE}}'",
"confusion-of-tongues": "Littattafai biyu ko fiye a cikin harsuna daban-daban za su shiga cikin bincike, wanda zai iya haifar da sakamako mai ruɗani.",
"welcome-page-overzealous-filter": "Babu sakamako. Kuna so a <a href=\"{{URL}}\">sake saita tace</a>?",
"powered-by-kiwix-html": "<a href=\"https://kiwix.org\">Kiwix</a> ne ke ƙarfafa shi",
"search": "Nema",
"book-filtering-all-categories": "Dukkanin nau'o'in",
"book-filtering-all-languages": "Duka harsuna",
"count-of-matching-books": "{{COUNT}} littafi(s)",
"download": "Sauke",
"direct-download-link-text": "Kai tsaye",
"direct-download-alt-text": "saukewa kai tsaye",
"hash-download-link-text": "Sha256 hash",
"hash-download-alt-text": "sauke hash",
"magnet-link-text": "Magnet link",
"magnet-alt-text": "Magnet ɗin saukewa",
"torrent-download-link-text": "Torrent fayil",
"torrent-download-alt-text": "download torrent",
"library-opds-feed-all-entries": "Ciyarwar OPDS Library - Duk shigarwar",
"filter-by-tag": "Tace da alamar \"{{TAG}}\"",
"stop-filtering-by-tag": "Dakatar da tacewa ta hanyar \"{{TAG}}\"",
"library-opds-feed-parameterised": " OPDS ciyar wa OPDS- entries matching {{#LANG}}\nLanguage: {{LANG}} {{/LANG}}{{#CATEGORY}}\nCategory: {{CATEGORY}} {{/CATEGORY}}{{#TAG}}\nTag: {{TAG}} {{/TAG}}{{#Q}}\nQuery: {{Q}} {{/Q}}",
"welcome-to-kiwix-server": "Barka da zowa manhajar Kiwix",
"download-links-heading": "Bring out ways through which people can join hands together .{{BOOK_TITLE}}",
"download-links-title": "Sauke littafin",
"preview-book": "Dubawa",
"unknown-error": "Kuskuren da ba a sani ba"
}

View File

@@ -14,7 +14,6 @@
"suggest-search": "לעשות חיפוש טקסט מלא עבור <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
"random-article-failure": "אוי! לא עבדה בחירת ערך אקראי :(",
"invalid-raw-data-type": "{{DATATYPE}} הוא לא בקשה תקינה של תוכן גולמי.",
"invalid-request": "הכתובת המבוקשת \"{{{url}}}\" אינה בקשה תקינה.",
"no-value-for-arg": "לא סופק ערך לארגומנט {{ARGUMENT}}",
"no-query": "לא סופקה שאילתה.",
"raw-entry-not-found": "לא ניתן למצוא את רשומת ה־{{DATATYPE}} בשם {{ENTRY}}",
@@ -22,30 +21,14 @@
"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": "שגיאת שרת פנימית",
"fulltext-search-unavailable": "חיפוש בטקסט מלא אינו זמין",
"no-search-results": "מנוע החיפוש בטקסט מלא אינו זמין עבור התוכן הזה.",
"search-results-page-title": "חיפוש: {{SEARCH_PATTERN}}",
"search-results-page-header": "תוצאות <b>{{START}} עד {{END}}</b> מתוך <b>{{COUNT}}</b> עבור <b>\"{{{SEARCH_PATTERN}}}\"</b>",
"empty-search-results-page-header": "לא נמצאו תוצאות עבור <b>\"{{{SEARCH_PATTERN}}}\"</b>",
"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>",
@@ -55,33 +38,19 @@
"count-of-matching-books": "{{COUNT}} ספרים",
"download": "הורדה",
"direct-download-link-text": "ישירה",
"direct-download-alt-text": "הורדה ישירה דרך HTTP(S)",
"hash-download-link-text": "סיכום ביקורת Sha256",
"hash-download-alt-text": צגת סיכום ביקורת SHA-256",
"direct-download-alt-text": "הורדה ישירה",
"hash-download-link-text": "גיבוב Sha256",
"hash-download-alt-text": ורדת גיבוב",
"magnet-link-text": "קישור Magnet",
"magnet-alt-text": "הורדה באמצעות קישור magnet",
"torrent-download-link-text": "ביטורנט",
"torrent-download-alt-text": "הורדה באמצעות ביטורנט",
"magnet-alt-text": "הורדת magnet",
"torrent-download-link-text": "קובץ טורנט",
"torrent-download-alt-text": "הורדת טורנט",
"library-opds-feed-all-entries": "הזנת ספריית OPDS - כל הרשומות",
"filter-by-tag": "לסנן לפי התג \"{{{TAG}}}\"",
"stop-filtering-by-tag": "להפסיק סינון לפי התג \"{{{TAG}}}\"",
"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}}",
"welcome-to-kiwix-server": "ברוך בואך לשרת קיוויקס",
"download-links-heading": "הורדת קישורים עבור <b><i>{{BOOK_TITLE}}</i></b>",
"download-links-title": "הורדת ספר",
"preview-book": "תצוגה מקדימה",
"unknown-error": "שגיאה בלתי־ידועה",
"book-category.gutenberg": "גוטנברג",
"book-category.vikidia": "ויקידיה",
"book-category.wikibooks": "ויקיספר",
"book-category.wikinews": "ויקיחדשות",
"book-category.wikipedia": "ויקיפדיה",
"book-category.wikiquote": "ויקיציטוט",
"book-category.wikisource": "ויקיטקסט (Wikisource)",
"book-category.wikispecies": "ויקימינים",
"book-category.wikiversity": "ויקיברסיטה",
"book-category.wikivoyage": "ויקימסע",
"book-category.wiktionary": "ויקימילון",
"book-category.other": "אחר",
"text-loading-content": "התוכן נטען"
"preview-book": "תצוגה מקדימה"
}

View File

@@ -1,66 +0,0 @@
{
"@metadata": {
"authors": [
"Abijeet Patro",
"Juuz0"
]
},
"name": "हिन्दी",
"suggest-full-text-search": "जिसमें '{{{SEARCH_TERMS}}}' शामिल है...",
"no-such-book": "ऐसी कोई किताब नहीं: {{BOOK_NAME}}",
"too-many-books": "बहुत सारी पुस्तकों का अनुरोध किया गया है ({{NB_BOOKS}}) जहां सीमा {{LIMIT}} है",
"no-book-found": "कोई भी पुस्तक चयन मानदंड से मेल नहीं खाती",
"url-not-found": "अनुरोधित URL \"{{url}}\" इस सर्वर पर नहीं मिला।",
"suggest-search": "<a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a> के लिए पूर्ण पाठ खोज करें",
"random-article-failure": "उफ़! एक यादृच्छिक लेख चुनने में विफल :(",
"invalid-raw-data-type": "{{DATATYPE}} कच्ची सामग्री के लिए वैध अनुरोध नहीं है।",
"no-value-for-arg": "तर्क के लिए कोई मूल्य प्रदान नहीं किया गया {{ARGUMENT}}",
"no-query": "कोई प्रश्न नहीं दिया गया.",
"raw-entry-not-found": "{{DATATYPE}} प्रविष्टि {{ENTRY}} नहीं मिल सकी",
"400-page-title": "अवैध आवेदन",
"400-page-heading": "अवैध आवेदन",
"404-page-title": "सामग्री नहीं मिली",
"404-page-heading": "नहीं मिला",
"500-page-title": "आंतरिक सर्वर त्रुटि",
"500-page-heading": "आंतरिक सर्वर त्रुटि",
"fulltext-search-unavailable": "पूर्णपाठ खोज अनुपलब्ध",
"no-search-results": "इस सामग्री के लिए पूर्णपाठ खोज इंजन उपलब्ध नहीं है।",
"library-button-text": "स्वागत पृष्ठ पर जाएँ",
"home-button-text": "'{{BOOK_TITLE}}' के मुख्य पृष्ठ पर जाएँ",
"random-page-button-text": "यादृच्छिक रूप से चयनित पृष्ठ पर जाएँ",
"searchbox-tooltip": "'{{BOOK_TITLE}}' खोजें",
"confusion-of-tongues": "विभिन्न भाषाओं की दो या दो से अधिक पुस्तकें खोज में भाग लेंगी, जिससे भ्रमित करने वाले परिणाम मिल सकते हैं।",
"welcome-page-overzealous-filter": "कोई परिणाम नहीं। क्या आप <a href=\"{{URL}}\">फ़िल्टर रीसेट करना</a> चाहेंगे?",
"powered-by-kiwix-html": "<a href=\"https://kiwix.org\">किविक्स</a> द्वारा संचालित",
"search": "खोजें",
"book-filtering-all-categories": "सब वर्ग",
"book-filtering-all-languages": "सभी भाषाएँ",
"count-of-matching-books": "{{COUNT}} पुस्तक/पुस्तकें",
"download": "डाउनलोड",
"direct-download-link-text": "प्रत्यक्ष",
"direct-download-alt-text": "प्रत्यक्षत: डाउनलोड",
"hash-download-link-text": "शा256 हैश",
"hash-download-alt-text": "हैश डाउनलोड करें",
"magnet-link-text": "MAGNET लिंक",
"magnet-alt-text": "MAGNET डाउनलोड करें",
"torrent-download-link-text": "टोरेंट फ़ाइल",
"torrent-download-alt-text": "टोरेंट डाउनलोड करें",
"library-opds-feed-all-entries": "लाइब्रेरी ओपीडीएस फ़ीड - सभी प्रविष्टियाँ",
"filter-by-tag": "टैग \"{{TAG}}\" द्वारा फ़िल्टर करें",
"stop-filtering-by-tag": "\"{{TAG}}\" टैग द्वारा फ़िल्टर करना बंद करें",
"library-opds-feed-parameterised": "लाइब्रेरी ओपीडीएस फ़ीड - मिलान वाली प्रविष्टियाँ {{#LANG}}\nभाषा: {{LANG}} {/LANG}}{{#CATEGORY}}\nश्रेणी: {{CATEGORY}} {/CATEGORY}} {{#TAG}}\nटैग: {{TAG}} {{/TAG}}{{#Q}}\nक्वेरी: {{Q}} {{/Q}}",
"welcome-to-kiwix-server": "कीविक्स सर्वर में आपका स्वागत है",
"download-links-heading": "<b><i>{{BOOK_TITLE}}</i></b> के लिए डाउनलोड लिंक",
"download-links-title": "पुस्तक डाउनलोड करें",
"preview-book": "पूर्वावलोकन",
"book-category.wikibooks": "विकिपुस्तक",
"book-category.wikinews": "विकिसमाचार",
"book-category.wikipedia": "विकिपीडिया",
"book-category.wikiquote": "विकिसूक्ति",
"book-category.wikisource": "विकिस्रोत",
"book-category.wikispecies": "विकिप्रजाति",
"book-category.wikiversity": "विकिविश्वविद्यालय",
"book-category.wikivoyage": "विकियात्रा",
"book-category.wiktionary": "विकिकोश",
"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,20 +13,9 @@
"400-page-heading": "Անվավեր հարցում",
"404-page-title": "Սխալ հասցե",
"404-page-heading": "Սխալ հասցե",
"500-img-text": "Էջը չի աշխատում",
"library-button-text": "Գրադարանի էջ",
"home-button-text": "Դեպի '{{BOOK_TITLE}}'֊ի գլխավոր էջը",
"random-page-button-text": "Բացել պատահական էջ",
"searchbox-tooltip": "Որոնել '{{BOOK_TITLE}}'֊ում",
"book-filtering-all-categories": "Բոլոր կատեգորիաներ",
"book-category.wikibooks": "Վիքիգրքեր",
"book-category.wikinews": "Վիքիլուրեր",
"book-category.wikipedia": "Վիքիպեդիա",
"book-category.wikiquote": "Վիքիքաղվածք",
"book-category.wikisource": "Վիքիդարան",
"book-category.wikispecies": "Վիքիցեղեր",
"book-category.wikiversity": "Վիքիլսարան",
"book-category.wikivoyage": "Վիքիճամփորդ",
"book-category.wiktionary": "Վիքիբառարան",
"book-category.other": "Այլ"
"book-filtering-all-categories": "Բոլոր կատեգորիաներ"
}

View File

@@ -13,8 +13,7 @@
"url-not-found": "Le URL reuqestate \"{{url}}\" non ha essite trovate sur iste servitor.",
"suggest-search": "Facer un recerca in texto complete de <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
"random-article-failure": "Ups! Non poteva eliger un articulo aleatori :(",
"invalid-raw-data-type": "{{DATATYPE}} non es un requesta valide pro contento brute.",
"invalid-request": "Le URL requestate “{{{url}}}” non es un requesta valide.",
"invalid-raw-data-type": "{{DATATYPE}} non es un request valide pro contento crude.",
"no-value-for-arg": "Necun valor fornite pro le argumento {{ARGUMENT}}",
"no-query": "Necun consulta fornite.",
"raw-entry-not-found": "Non pote trovar le entrata {{ENTRY}} del typo {{DATATYPE}}",
@@ -22,36 +21,14 @@
"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",
"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}}",
"search-results-page-header": "Resultatos <b>{{START}}-{{END}}</b> de <b>{{COUNT}}</b> pro <b>“{{{SEARCH_PATTERN}}}”</b>",
"empty-search-results-page-header": "Necun resultato ha essite trovate pro <b>“{{{SEARCH_PATTERN}}}”</b>",
"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>",
@@ -61,31 +38,19 @@
"count-of-matching-books": "{{COUNT}} libro(s)",
"download": "Discargar",
"direct-download-link-text": "Directe",
"direct-download-alt-text": "Discargamento directe per HTTP(S)",
"hash-download-link-text": "Summa de controlo SHA-256",
"hash-download-alt-text": "Monstrar le summa de controlo SHA-256 del file",
"direct-download-alt-text": "discargamento directe",
"hash-download-link-text": "Hash SHA256",
"hash-download-alt-text": "hash del discargamento",
"magnet-link-text": "Ligamine Magnet",
"magnet-alt-text": "Discargar con ligamine Magnet",
"torrent-download-link-text": "BitTorrent",
"torrent-download-alt-text": "Discargar per medio de BitTorrent",
"magnet-alt-text": "ligamine \"magnet\" de discargamento",
"torrent-download-link-text": "File Torrent",
"torrent-download-alt-text": "discargar Torrent",
"library-opds-feed-all-entries": "Fluxo OPDS del bibliotheca Tote le entratas",
"filter-by-tag": "Filtrar per etiquetta “{{{TAG}}}”",
"stop-filtering-by-tag": "Non plus filtrar per etiquetta “{{{TAG}}}”",
"filter-by-tag": "Filtrar per etiquetta \"{{TAG}}\"",
"stop-filtering-by-tag": "Non plus filtrar per etiquetta \"{{TAG}}\"",
"library-opds-feed-parameterised": "Fluxo OPDS del bibliotheca Entratas correspondente a {{#LANG}}\nLingua {{LANG}} {{/LANG}}{{#CATEGORY}}\nCategoria: {{CATEGORY}} {{/CATEGORY}}{{#TAG}}\nEtiquetta: {{TAG}} {{/TAG}}{{#Q}}\nConsulta: {{Q}} {{/Q}}",
"welcome-to-kiwix-server": "Benvenite al servitor Kiwix",
"download-links-heading": "Discargar ligamines pro <b><i>{{BOOK_TITLE}}</i></b>",
"download-links-title": "Discargar libro",
"preview-book": "Previsualisation",
"unknown-error": "Error incognite",
"book-category.wikibooks": "Wikilibros",
"book-category.wikinews": "Wikinovas",
"book-category.wikipedia": "Wikipedia",
"book-category.wikiquote": "Wikiquote",
"book-category.wikisource": "Wikisource",
"book-category.wikispecies": "Wikispecies",
"book-category.wikiversity": "Wikiversitate",
"book-category.wikivoyage": "Wikiviage",
"book-category.wiktionary": "Wiktionario",
"book-category.other": "Altere",
"text-loading-content": "Carga contento"
"preview-book": "Previsualisation"
}

View File

@@ -1,74 +0,0 @@
{
"@metadata": {
"authors": [
"Akmaie Ajam"
]
},
"name": "Bahasa Inggris",
"suggest-full-text-search": "mengandung '{{{SEARCH_TERMS}}}'...",
"no-such-book": "Tidak ada buku seperti ini: {{BOOK_NAME}}",
"too-many-books": "Terlalu banyak buku yang diminta ({{NB_BOOKS}}) dimana batasnya adalah {{LIMIT}}",
"no-book-found": "Tidak ada buku yang sesuai kriteria yang dipilih",
"url-not-found": "URL yang diminta \"{{url}}\" tidak ditemukan di server ini.",
"suggest-search": "Lakukan pencarian teks lengkap untuk <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
"random-article-failure": "Waduh! Gagal memilih artikel acak :(",
"invalid-raw-data-type": "{{DATATYPE}} bukan permintaan yang sah untuk konten mentah.",
"invalid-request": "URL yang diminta \"{{{url}}}\" bukan permintaan yang sah.",
"no-value-for-arg": "Tidak ada nilai yang diberikan untuk argumen {{ARGUMENT}}",
"no-query": "Tidak ada kueri yang diberikan.",
"raw-entry-not-found": "Tidak dapat menemukan entri {{DATATYPE}} {{ENTRY}}",
"400-page-title": "Permintaan tidak sah",
"400-page-heading": "Permintaan tidak sah",
"404-page-title": "Konten tidak ditemukan",
"404-page-heading": "Tidak Ditemukan",
"500-page-title": "Kesalahan Server Internal",
"500-page-heading": "Kesalahan Server Internal",
"500-page-text": "Terjadi kesalahan server internal. Kami mohon maaf atas hal ini :/",
"fulltext-search-unavailable": "Pencarian teks lengkap tidak tersedia",
"no-search-results": "Mesin pencari teks lengkap tidak tersedia untuk konten ini.",
"search-results-page-title": "Pencarian: {{SEARCH_PATTERN}}",
"search-results-page-header": "Hasil <b>{{START}}-{{END}}</b> dari <b>{{COUNT}}</b> untuk <b>\"{{{SEARCH_PATTERN}}}\"</b>",
"empty-search-results-page-header": "Tidak ada hasil yang ditemukan untuk <b>\"{{{SEARCH_PATTERN}}}\"</b>",
"search-result-book-info": "dari {{BOOK_TITLE}}",
"word-count": "{{COUNT}} kata",
"library-button-text": "Pergi ke halaman selamat datang",
"home-button-text": "Buka halaman utama '{{BOOK_TITLE}}'",
"random-page-button-text": "Buka halaman yang dipilih secara acak",
"searchbox-tooltip": "Cari '{{BOOK_TITLE}}'",
"confusion-of-tongues": "Dua buku atau lebih dalam bahasa berbeda dapat muncul dalam hasil pencarian, yang dapat memicu hasil yang membingungkan.",
"welcome-page-overzealous-filter": "Tidak ada hasil. Apakah Anda ingin <a href=\"{{URL}}\">mengatur ulang filter</a>?",
"powered-by-kiwix-html": "Didukung oleh <a href=\"https://kiwix.org\">Kiwix</a>",
"search": "Cari",
"book-filtering-all-categories": "Semua kategori",
"book-filtering-all-languages": "Semua bahasa",
"count-of-matching-books": "{{COUNT}} buku",
"download": "Unduh",
"direct-download-link-text": "Langsung",
"direct-download-alt-text": "Unduh langsung melalui HTTP(S)",
"hash-download-link-text": "Checksum SHA-256",
"hash-download-alt-text": "Tampilkan checksum berkas SHA-256",
"magnet-link-text": "Tautan magnet",
"magnet-alt-text": "Unduh melalui tautan Magnet",
"torrent-download-link-text": "BitTorrent",
"torrent-download-alt-text": "Unduh melalui BitTorrent",
"library-opds-feed-all-entries": "Umpan OPDS Perpustakaan - Semua entri",
"filter-by-tag": "Saring berdasarkan tag \"{{{TAG}}}\"",
"stop-filtering-by-tag": "Berhenti penyaringan berdasarkan tag \"{{{TAG}}}\"",
"library-opds-feed-parameterised": "Umpan OPDS Perpustakaan - entri yang cocok dengan {{#LANG}}\nBahasa: {{LANG}} {{/LANG}}{{#CATEGORY}}\nKategori: {{CATEGORY}} {{/CATEGORY}}{{#TAG}}\nTag: {{TAG}} {{/TAG}}{{#Q}}\nKueri: {{Q}} {{/Q}}",
"welcome-to-kiwix-server": "Selamat datang di Server Kiwix",
"download-links-heading": "Tautan unduhan untuk <b><i>{{BOOK_TITLE}}</i></b>",
"download-links-title": "Unduh buku",
"preview-book": "Pratayang",
"unknown-error": "Kesalahan yang tidak diketahui",
"book-category.wikibooks": "Wikibuku",
"book-category.wikinews": "Wikiberita",
"book-category.wikipedia": "Wikipedia",
"book-category.wikiquote": "Wikikutip",
"book-category.wikisource": "Wikisumber",
"book-category.wikispecies": "Wikispesies",
"book-category.wikiversity": "Wikiversitas",
"book-category.wikivoyage": "Wikiwisata",
"book-category.wiktionary": "Wikikamus",
"book-category.other": "Lainnya",
"text-loading-content": "Memuat Konten"
}

View File

@@ -1,65 +0,0 @@
{
"@metadata": {
"authors": [
"Accuratecy051",
"Ngostary2k",
"Oby Ezeilo"
]
},
"name": "Bekee",
"suggest-full-text-search": "nwere {{{SEARCH_TERMS}}}'",
"no-such-book": "Enweghị akwụkwọ dị otú a: {{BOOK_NAME}}",
"too-many-books": "Arịrịọ ọtụtụ akwụkwọ ({{NB_BOOKS}}) ebe oke bụ {{LIMIT}}",
"no-book-found": "Ọ nweghị akwụkwọ dabara na nhọpụta nhọrọ",
"url-not-found": "Ahụghị URL a rịọrọ \"{{url}}\" na nkesa a.",
"suggest-search": "Mee ọchụchọ ederede zuru oke maka <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
"random-article-failure": "Ee! Ịhọrọ akụkọ enweghị usoro :(",
"invalid-raw-data-type": "{{DATATYPE}} abụghị arịrịọ ziri ezi maka ọdịnaya raw.",
"invalid-request": "Arịrịọ gbasara URL \"{{{url}}}\" e zighi ezi.",
"no-value-for-arg": "Ọ nweghị uru enyere maka arụmụka {{ARGUMENT}}",
"no-query": "Ọnweghị ajụjụ enyere.",
"raw-entry-not-found": "Enweghị ike ịchọta ntinye {{DATATYPE}} {{ENTRY}}",
"400-page-title": "Arịrịọ na-ezighi ezi",
"400-page-heading": "Arịrịọ na-ezighi ezi",
"404-page-title": "Ahụghị ọdịnaya",
"404-page-heading": "Ahụghị",
"500-page-title": "Mperi Sava Ime",
"500-page-heading": "Mperi Sava Ime",
"500-page-text": "Enwere mperi ihe nkesa dị n'ime. Ọ dị anyị nwute na nke ahụ :/",
"fulltext-search-unavailable": "Ọchịchọ ederede zuru ezu adịghị",
"no-search-results": "Igwe nchọta ederede zuru oke adịghị maka ọdịnaya a.",
"search-results-page-title": "Chọọ: {{SEARCH_PATTERN}}",
"search-results-page-header": "Rịzọlt ga <b>{{START}}-{{END}}</b> nke <b>{{COUNT}}</b> maka <b>\"{{{SEARCH_PATTERN}}}\"</b>",
"empty-search-results-page-header": "Ọnweghị rịzọlt ahụrụ maka <b>\"{{{SEARCH_PATTERN}}}\"</b>",
"search-result-book-info": "sitere na {{BOOK_TITLE}}",
"word-count": "Okwu {{COUNT}}",
"library-button-text": "Gaa na ibe nnabata",
"home-button-text": "Gaa na isi ibe nke '{{BOOK_TITLE}}'",
"random-page-button-text": "Gaa na ibe ahọpụtara enweghị usoro",
"searchbox-tooltip": "Chọọ '{{BOOK_TITLE}}'",
"confusion-of-tongues": "Akwụkwọ abụọ ma ọ bụ karịa n'asụsụ dị iche iche ga-esonye na nchọ, nke nwere ike ibute nsonaazụ mgbagwoju anya.",
"welcome-page-overzealous-filter": "Enweghị nsonaazụ. Ọ ga-amasị gị <a href=\"{{URL}}\">ịtọgharịa nzacha</a> ?",
"powered-by-kiwix-html": "<a href=\"https://kiwix.org\">Kiwix</a> kwadoro ya",
"search": "Chọọ",
"book-filtering-all-categories": "Nkeji niile",
"book-filtering-all-languages": "Asụsụ niile",
"count-of-matching-books": "akwụkwọ {{COUNT}}",
"download": "Budata",
"direct-download-link-text": "Gosi",
"direct-download-alt-text": "nbudata ozugbo",
"hash-download-link-text": "Sha256 hash",
"hash-download-alt-text": "budata hash",
"magnet-link-text": "Njikọ magnet",
"magnet-alt-text": "ibudata magnet",
"torrent-download-link-text": " faịlụ nke Torrent",
"torrent-download-alt-text": "Budata torrent",
"library-opds-feed-all-entries": "Ọbá akwụkwọ OPDS Feed - Ihe niile",
"filter-by-tag": "Wepụta site na mkpado \"{{TAG}}\"",
"stop-filtering-by-tag": "Kwụsị nzacha site na mkpado \"{{TAG}}\"",
"library-opds-feed-parameterised": "Ọbá akwụkwọ OPDS nri - ndenye dabara na {{#LANG}}\nAsụsụ: {{LANG}} {{/LANG}}{{#CATEGORY}}\n Kategori: {{CATEGORY}} {{/CATEGORY}} {{#TAG}}\nTag: {{TAG}} {{/TAG}}{{#Q}}\n Ajụjụ: {{Q}} {{/Q}}",
"welcome-to-kiwix-server": "Nabata na Kiwix Server",
"download-links-heading": "Budata njikọ maka <b><i>{{BOOK_TITLE}}</i></b>",
"download-links-title": "Budata akwụkwọ",
"preview-book": "Ziwe nkirimaàtụ̀",
"unknown-error": "amaghị m njehie"
}

View File

@@ -3,10 +3,7 @@
"authors": [
"Albano",
"Beta16",
"Clorofolle",
"Luca.favorido",
"McDutchie",
"Wheelygay"
"McDutchie"
]
},
"name": "italiano",
@@ -17,53 +14,21 @@
"url-not-found": "L'URL richiesto \"{{url}}\" non è stato trovato in questo server.",
"suggest-search": "Effettua una ricerca di testo completo per <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
"random-article-failure": "Ops! Impossibile selezionare un articolo casuale :(",
"invalid-request": "L'URL richiesto \"{{{url}}}\" non è una richiesta valida.",
"no-value-for-arg": "Nessun valore fornito per l'argomento {{ARGUMENT}}",
"400-page-title": "Richiesta non valida",
"400-page-heading": "Richiesta non valida",
"404-page-title": "Contenuto non trovato",
"404-page-heading": "Non trovato",
"new-404-page-title": "Pagina non trovata",
"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!",
"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",
"500-page-heading": "Errore interno del server",
"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}}}'",
"welcome-page-overzealous-filter": "Nessun risultato. Vuoi <a href=\"{{URL}}\">reimpostare il filtro</a>?",
"search": "Cerca",
"searchbox-tooltip": "Cerca '{{BOOK_TITLE}}'",
"book-filtering-all-categories": "Tutte le categorie",
"book-filtering-all-languages": "Tutte le lingue",
"count-of-matching-books": "{{COUNT}} libro/i",
"download": "Scarica",
"direct-download-link-text": "Download diretto",
"direct-download-alt-text": "Scarica direttamente tramite HTTP(S)",
"magnet-alt-text": "Scarica tramite collegamento Magnet",
"torrent-download-link-text": "BitTorrent",
"torrent-download-alt-text": "Scarica tramite BitTorrent",
"welcome-to-kiwix-server": "Benvenuti al server Kiwix",
"download-links-heading": "Link per scaricare <b><i>{{BOOK_TITLE}}</i></b>",
"download-links-title": "Scarica libro",
"preview-book": "Anteprima",
"unknown-error": "Errore sconosciuto",
"book-category.wikibooks": "Wikibooks",
"book-category.wikinews": "Wikinotizie",
"book-category.wikipedia": "Wikipedia",
"book-category.wikiquote": "Wikiquote",
"book-category.wikisource": "Wikisource",
"book-category.wikispecies": "Wikispecies",
"book-category.wikiversity": "Wikiversità",
"book-category.wikivoyage": "Wikivoyage",
"book-category.wiktionary": "Wikizionario",
"book-category.other": "Altro",
"text-loading-content": "Caricamento contenuto"
"preview-book": "Anteprima"
}

View File

@@ -1,80 +1,19 @@
{
"@metadata": {
"authors": [
"YeBoy371",
"Ykhwong"
]
},
"name": "한국어",
"suggest-full-text-search": "'{{{SEARCH_TERMS}}}' 포함...",
"no-such-book": "해당 책이 없습니다: {{BOOK_NAME}}",
"too-many-books": "요청된 책이 너무 많습니다. ({{NB_BOOKS}}) 한도는 {{LIMIT}}입니다.",
"no-book-found": "선택 기준에 해당하는 책이 없습니다",
"url-not-found": "\"{{url}}\" 요청 URL은 이 서버에서 찾을 수 없습니다.",
"suggest-search": "<a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>에 대한 전문 검색을 수행해 보세요",
"random-article-failure": "이런! 임의 문서를 선택하지 못했습니다 :(",
"invalid-raw-data-type": "{{DATATYPE}} 값은 원시 콘텐츠에 대한 유효한 요청이 아닙니다.",
"invalid-request": "\"{{{url}}}\" 요청 URL은 유효한 요청이 아닙니다.",
"no-value-for-arg": "{{ARGUMENT}} 인수에 지정된 값이 없습니다",
"no-query": "지정된 쿼리가 없습니다.",
"raw-entry-not-found": "{{DATATYPE}} 항목인 {{ENTRY}} 항목을 찾을 수 없습니다",
"400-page-title": "잘못된 요청",
"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": "내부 서버 오류",
"fulltext-search-unavailable": "전문 검색을 사용할 수 없습니다",
"no-search-results": "이 콘텐츠에는 전문 검색 엔진을 사용할 수 없습니다.",
"search-results-page-title": "검색: {{SEARCH_PATTERN}}",
"search-results-page-header": "<b>\"{{{SEARCH_PATTERN}}}\"</b>에 대한 <b>{{COUNT}}</b>개 중 <b>{{START}}-{{END}}</b> 결과",
"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}}}' 검색",
"welcome-page-overzealous-filter": "결과가 없습니다. <a href=\"{{URL}}\">필터를 재설정</a>하시겠습니까?",
"powered-by-kiwix-html": "<a href=\"https://kiwix.org\">Kiwix</a>에서 제공",
"search": "검색",
"book-filtering-all-categories": "모든 분류",
"book-filtering-all-languages": "모든 언어",
"count-of-matching-books": "책 {{COUNT}}권",
"download": "다운로드",
"direct-download-link-text": "직접",
"direct-download-alt-text": "HTTP(S)를 통해 직접 다운로드",
"hash-download-link-text": "SHA-256 체크섬",
"hash-download-alt-text": "SHA-256 파일 체크섬 표시",
"magnet-link-text": "마그넷 링크",
"magnet-alt-text": "마그넷 링크를 통해 다운로드",
"torrent-download-link-text": "비트토렌트",
"torrent-download-alt-text": "비트토렌트를 통해 다운로드",
"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}}",
"welcome-to-kiwix-server": "Kiwix Server에 오신 것을 환영합니다",
"download-links-heading": "<b><i>{{BOOK_TITLE}}</i></b>의 링크 다운로드",
"download-links-title": "책 다운로드",
"preview-book": "미리 보기",
"unknown-error": "알 수 없는 오류",
"book-category.wikibooks": "위키책",
"book-category.wikinews": "위키뉴스",
"book-category.wikipedia": "위키백과",
"book-category.wikiquote": "위키인용집",
"book-category.wikisource": "위키문헌",
"book-category.wikispecies": "위키생물종",
"book-category.wikiversity": "위키배움터",
"book-category.wikivoyage": "위키여행",
"book-category.wiktionary": "위키낱말사전",
"book-category.other": "기타"
"preview-book": "미리 보기"
}

View File

@@ -7,51 +7,21 @@
]
},
"name": "Lëtzebuergesch",
"suggest-full-text-search": "enthält '{{{SEARCH_TERMS}}}'...",
"no-such-book": "Buch net fonnt: {{BOOK_NAME}}",
"too-many-books": "Ze vill Bicher ugefrot ({{NB_BOOKS}}), d'Limitt läit bei {{LIMIT}}",
"url-not-found": "Déi ugefroten URL „{{url}}“ gouf op dësem Server net fonnt.",
"suggest-search": "Maacht eng Volltext-Sich fir <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
"random-article-failure": "Ups! Et konnt keen zoufällegen Artikel ausgewielt ginn :(",
"404-page-title": "Inhalt net fonnt",
"404-page-heading": "Net fonnt",
"new-404-page-title": "Säit net fonnt",
"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-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!",
"500-page-heading": "Interne Feeler um Server",
"fulltext-search-unavailable": "Volltext-Sich net verfügbar",
"search-results-page-title": "Sichen: {{SEARCH_PATTERN}}",
"search-results-page-header": "Resultater <b>{{START}}-{{END}}</b> vu(n) <b>{{COUNT}}</b> fir <b>„{{{SEARCH_PATTERN}}}“</b>",
"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",
"book-filtering-all-languages": "All Sproochen",
"count-of-matching-books": "{{COUNT}} Buch/Bicher",
"download": "Eroflueden",
"direct-download-link-text": "Direkt",
"torrent-download-link-text": "BitTorrent",
"download-links-title": "Buch eroflueden",
"unknown-error": "Onbekannte Feeler",
"book-category.stack_exchange": "Stack Exchange",
"book-category.wikibooks": "Wikibooks",
"book-category.wikinews": "Wikinews",
"book-category.wikipedia": "Wikipedia",
"book-category.wikiquote": "Wikiquote",
"book-category.wikisource": "Wikisource",
"book-category.wikispecies": "Wikispecies",
"book-category.wikiversity": "Wikiversity",
"book-category.wikivoyage": "Wikivoyage",
"book-category.wiktionary": "Wiktionnaire",
"book-category.other": "Anerer"
"direct-download-link-text": "Direkt"
}

View File

@@ -14,7 +14,6 @@
"suggest-search": "Побарајте го <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a> по целиот текст",
"random-article-failure": "Упс! Не успеав да изберам случајна статија :(",
"invalid-raw-data-type": "{{DATATYPE}} не претставува важечко барање за сирова содржина.",
"invalid-request": "Побараната URL „{{{url}}}“ не претставува важечко барање.",
"no-value-for-arg": "Нема укажано вредност за аргументот {{ARGUMENT}}",
"no-query": "Не е укажано барање.",
"raw-entry-not-found": "Не можам да ја најдам {{DATATYPE}}-ставката {{ENTRY}}",
@@ -22,36 +21,14 @@
"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": "Внатрешна грешка во опслужувачот",
"fulltext-search-unavailable": "Целотекстното пребарување е недостапно",
"no-search-results": "Погонот за целотекстно пребарување не е достапен за оваа содржина.",
"search-results-page-title": "Пребарување: {{SEARCH_PATTERN}}",
"search-results-page-header": "Исход <b>{{START}}-{{END}}</b> од <b>{{COUNT}}</b> за <b>„{{{SEARCH_PATTERN}}}“</b>",
"empty-search-results-page-header": "Не најдов ништо за <b>„{{{SEARCH_PATTERN}}}“</b>",
"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>",
@@ -61,39 +38,19 @@
"count-of-matching-books": "{{COUNT}} книги",
"download": "Преземи",
"direct-download-link-text": "Непосредно",
"direct-download-alt-text": "Непосредно преземање преку HTTP(S)",
"hash-download-link-text": "Контролен збир Sha256",
"hash-download-alt-text": "Прикажи контролен збир SHA-256 на податотеката",
"direct-download-alt-text": "непосредно преземање",
"hash-download-link-text": "Sha256-тараба",
"hash-download-alt-text": "преземи тараба",
"magnet-link-text": "Магнетна врска",
"magnet-alt-text": "Преземи преку Magnet-врска",
"torrent-download-link-text": "BitTorrent",
"torrent-download-alt-text": "Преземи преку BitTorrent",
"magnet-alt-text": "преземи магнет",
"torrent-download-link-text": "Торентна податотека",
"torrent-download-alt-text": "преземи торент",
"library-opds-feed-all-entries": "Библиотечен тековник на OPDS — Сите ставки",
"filter-by-tag": "Филтрирај по ознаката „{{{TAG}}}“",
"stop-filtering-by-tag": "Запри филтрирање по ознаката „{{{TAG}}}“",
"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}}",
"welcome-to-kiwix-server": "Добре дојдовте на Опслужувачот на Кивикс",
"download-links-heading": "Врски за преземање на <b><i>{{BOOK_TITLE}}</i></b>",
"download-links-title": "Преземи книга",
"preview-book": "Преглед",
"unknown-error": "Непозната грешка",
"book-category.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": "Викидија",
"book-category.wikibooks": "Викикниги",
"book-category.wikihow": "wikiHow",
"book-category.wikinews": "Викивести",
"book-category.wikipedia": "Википедија",
"book-category.wikiquote": "Викицитат",
"book-category.wikisource": "Викиизвор",
"book-category.wikispecies": "Викивидови",
"book-category.wikiversity": "Викиуниверзитет",
"book-category.wikivoyage": "Википатување",
"book-category.wiktionary": "Викиречник",
"book-category.other": "друго",
"text-loading-content": "Ја вчитувам содржината"
"preview-book": "Преглед"
}

View File

@@ -1,20 +0,0 @@
{
"@metadata": {
"authors": [
"Tofeiku"
]
},
"name": "Bahasa Melayu",
"404-page-heading": "Tidak Dijumpai",
"500-page-title": "Ralat Pelayan Dalaman",
"500-page-heading": "Ralat Pelayan Dalaman",
"library-button-text": "Pergi ke laman selamat datang",
"searchbox-tooltip": "Cari '{{BOOK_TITLE}}'",
"search": "Cari",
"book-filtering-all-categories": "Semua kategori",
"book-filtering-all-languages": "Semua bahasa",
"download": "Muat turun",
"direct-download-link-text": "Langsung",
"direct-download-alt-text": "muat turun langsung",
"download-links-title": "Muat turun buku"
}

Some files were not shown because too many files have changed in this diff Show More