Compare commits

..

146 Commits

Author SHA1 Message Date
Veloman Yunkan
f8fc60ed2f Enabled smart mode of suggestions 2025-05-30 12:33:05 +04:00
Kelson
222e4396c7 Merge pull request #1195 from kiwix/translatewiki
Localisation updates from https://translatewiki.net.
2025-05-19 14:26:47 +02:00
translatewiki.net
4c480952d1 Localisation updates from https://translatewiki.net. 2025-05-19 14:07:14 +02:00
Kelson
79479788f9 Merge pull request #1178 from kiwix/nicer_error_pages
Nicer 404 error and external link blocker pages
2025-05-17 13:56:46 +02:00
Veloman Yunkan
3cd1f7854a Fully translated external link blocker page 2025-05-14 21:40:23 +04:00
Veloman Yunkan
58a211d01d Renamed a parameter in external link blocker template 2025-05-14 21:37:52 +04:00
Veloman Yunkan
07fc40da5a Translation works on external link blocker
This comes at the cost of broken support for SeaMonkey (due to usage of
import.meta in i18n.js)
2025-05-14 21:36:43 +04:00
Veloman Yunkan
d961447e1e Started translating the external link blocker page
The external link blocker page isn't actually translated since it is not
managed by the viewer. Will port the translation code from the viewer.js
in next commit.
2025-05-14 21:35:29 +04:00
Veloman Yunkan
c9ebeb7b96 New external link blocker page
The page doesn't support translation yet.
2025-05-14 21:34:14 +04:00
Veloman Yunkan
2d73ed31a9 Handling translation in ServerTest.HttpSexy404HtmlError
The failing test point in the ServerTest.Http404HtmlError unit-test
has been superseded by the enhanced ServerTest.HttpSexy404HtmlError
unit-test, resulting in a clean test-suite.
2025-05-14 21:30:05 +04:00
Veloman Yunkan
6a0349e575 Preparing ServerTest.HttpSexy404HtmlError for translation 2025-05-14 21:30:02 +04:00
Veloman Yunkan
6d80edc04a Translated the advice on the new 404 error page
Translation of the multi-line/multi-paragraph advice is done under the
assumption that its structure  (5 paragraphs, two of which serve as
entries in a bulleted list) can be preserved in the translations by
proper phrasing, i.e. the advice must be translated as a whole rather
than each of its sentences (which act as units of translation) separately.
2025-05-14 21:27:56 +04:00
Veloman Yunkan
b3a33747f0 More simple translations on the new 404 error page 2025-05-14 21:25:59 +04:00
Veloman Yunkan
f47490e1bc Started translation of the new 404 error page
- Enabled translation on the new 404 error page
- Translated its title & heading

This commit also fixes the failure of the ServerTest.UserLanguageControl
unit-test.

The only remaining failing test case is ServerTest.Http404HtmlError
(only for url=/ROOT%23%3F/content/zimfile/invalid-article?userlang=test).
2025-05-14 21:23:34 +04:00
Veloman Yunkan
1ce909ae68 ServerTest.HttpSexy404HtmlError unit-test
Converted a few failing test points of the ServerTest.Http404HtmlError
unit-test into a new unit-test ServerTest.HttpSexy404HtmlError.

Some broken test cases still remain.
2025-05-14 21:20:00 +04:00
Veloman Yunkan
5eb31d7286 New 404 error page
The page doesn't support translation, yet.

The new 404 error page is used only when accessing ZIM file content
(i.e. as a response from the `/content` API endpoint).

One notable difference from the previous error page is that now no hint
is provided about whether the error is due to trying to access a
non-existent book/ZIM-file or non-existent resource inside a valid
book/ZIM-file (previously such a hint was present in the suggested
search URL). However, when displayed in the viewer this difference can
be seen in the viewer toolbar - book related buttons are hidden if the
URL points to a non-existent book.

This change breaks some unit tests. They will be fixed in a separate
commit.
2025-05-14 21:18:36 +04:00
Veloman Yunkan
107421cdab Merge pull request #1179 from kiwix/translatewiki
Localisation updates from https://translatewiki.net.
2025-05-10 17:21:29 +04:00
Veloman Yunkan
bd474b9720 Updated/regenerated static/skin/languages.js 2025-05-10 17:12:19 +04:00
translatewiki.net
e66ba1a532 Localisation updates from https://translatewiki.net. 2025-05-10 17:12:19 +04:00
Veloman Yunkan
c1e58331d7 Merge pull request #1194 from kiwix/linux_ci_runner_upgrade
Upgraded Linux CI runner to ubuntu-22.04
2025-05-10 17:11:57 +04:00
Veloman Yunkan
d776077c5f Upgraded Linux CI runner to ubuntu-22.04
We are quite late with this change and were simply forced to make it
because ubuntu-20.04 has been phased out in GitHub Workflows.
2025-05-10 17:00:52 +04:00
Kelson
b8e997f805 Merge pull request #1190 from kiwix/pastproof_atomics_check
Made atomics check work with old compilers
2025-04-02 18:47:21 +02:00
Veloman Yunkan
b7421d7dae Made atomics check work with old compilers
In our CI a quite old version of gcc (6.3.0) is used for the aarch64
configs and it was confused by the (previous) code of the test program
intended to find out if libatomics must be explicitly passed to the
linker.
2025-04-02 15:10:46 +04:00
Kelson
a0c99f879b Merge pull request #1183 from Optimus-NP/kiwix-tools-issue_731
Show spinner while loading ZIM content in viewer iframe
2025-03-30 19:51:09 +02:00
Naman Pahwa
c7e86c9dbb Show spinner while loading ZIM content in viewer iframe
- Implemented a spinner to improve user experience while ZIM content is loading in the viewer iframe.
- Added .loader and .spinner styles in kiwix.css.
- The iframe content is initially hidden (visibility: hidden) and will be displayed once loading completes.
- Used CSS animations (@keyframes spin) for a smooth rotating effect.
2025-03-26 21:51:43 +05:30
Kelson
ad58a501b0 Merge pull request #1181 from Optimus-NP/kiwix-tools_issues_724
Replace multiple comma-separated languages with 'mul'
2025-03-17 18:55:07 +01:00
Naman Pahwa
a55e8565d1 Replace multiple comma-separated languages with 'mul'
- Refactored language code handling to replace multiple comma-separated values with 'mul'.
- Single-language entries remain unchanged.
- created the helper function to get the lang tag
- ensured the consistent behaviour for js and nojs version for the kiwix library view
2025-03-17 21:10:53 +05:30
Kelson
610b8cbb2a Merge pull request #1177 from kiwix/handling_of_pdf_links_under_chrome_on_android
If PDF viewer is not enabled PDFs are downloaded instead
2025-02-07 05:35:53 +01:00
Veloman Yunkan
e087f1c82f If PDF viewer is not enabled, PDFs are downloaded
Chrome on Android doesn't support displaying PDF documents inline so an
attempt to load a PDF into the Kiwix viewer iframe fails in a way that
may be confusing to the users. In such situations it is better to offer
to the users to download the PDF file so that they can view it with a
dedicated application. Making the clicked PDF link to open in a new
tab/window achieves exactly that effect.
2025-02-07 05:32:30 +01:00
Kelson
e5d3e6ff07 Merge pull request #1170 from kiwix/translatewiki
Localisation updates from https://translatewiki.net.
2025-02-07 05:32:01 +01:00
translatewiki.net
d0aeac64d0 Localisation updates from https://translatewiki.net. 2025-02-06 13:05:48 +01:00
Kelson
5da88c0ad7 Merge pull request #1176 from kiwix/handling_of_target_blank_external_links
Handling of external links with target="_blank"
2025-02-03 14:44:15 +01:00
Veloman Yunkan
c7d3a38a3e Handling of external links with target="_blank"
External links with target="_blank" attribute are opened in a new tab/window.

Also included metaKey (Command key under Mac) in the list of click
modifiers that make the link to be opened in a new tab.
2025-02-03 17:11:29 +04:00
Kelson
6b74395455 Merge pull request #1175 from kiwix/fully_visible_suggestions
Autocompleter box size matches the content width
2025-02-03 13:25:35 +01:00
Veloman Yunkan
2eea6136d6 Full suggestion text is shown on hover
The width of the suggestion box is capped at 600 pixels. The full
text of a suggestion is shown on hover (doesn't work on mobile).
2025-02-03 16:11:53 +04:00
Veloman Yunkan
64eb0d10d6 Autocompleter box size matches the content width
Note however that no upper limit is set on the width of the
autocompleter box - it is possible, but I don't see how we could
come up with a good value for it.
2025-01-27 17:20:39 +04:00
Kelson
664944f16c Merge pull request #1174 from OlCe2/main
meson.build: Comment on 'threads' dependency required for FreeBSD
2025-01-15 17:51:23 +01:00
Olivier Certner
d34a0c5bf0 meson.build: Comment on 'threads' dependency required for FreeBSD
While here, wrap the long test line.
2025-01-13 18:43:28 +01:00
Kelson
bb65d77229 Merge pull request #1169 from kiwix/wait-1s-to-aria2c
Wait up to 1s to le aria2c to start
2025-01-12 12:17:56 +01:00
Emmanuel Engelhart
98849831da Wait up to 1s to le aria2c to start 2025-01-12 12:17:10 +01:00
Kelson
2e3eae5615 Merge pull request #1173 from OlCe2/main
Two compilation fixes for FreeBSD
2025-01-08 16:47:14 +01:00
Olivier Certner
93ace5cf45 networkTools: Fix compilation on FreeBSD
Header <netinet/in.h> should be included (as per POSIX) to get a definition for
'struct sockaddr_in'.  Fixes commit "add ipv6 support to HTTP daemon".
2025-01-08 15:07:30 +01:00
Olivier Certner
cb777ed836 meson.build: Test whether linking with 'libatomic' is necessary
See the added comments for more details.

While here, initialize 'extra_deps' once and for all at the top and append to it
when needed.
2025-01-08 15:07:00 +01:00
Kelson
27e7840cce Merge pull request #1158 from kiwix/translatewiki
Localisation updates from https://translatewiki.net.
2024-12-20 10:18:53 +01:00
translatewiki.net
99c28b72b5 Localisation updates from https://translatewiki.net. 2024-12-20 10:11:36 +01:00
Kelson
81b579cdcb Merge pull request #1168 from kiwix/remove-set-output-in-workflows
Update a few dependencies in the workflows
2024-12-20 10:08:08 +01:00
Emmanuel Engelhart
f693f700bc Remove HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1 2024-12-20 09:13:26 +01:00
Emmanuel Engelhart
50f04d7060 'pkg-config' is already installed with proper version 2024-12-20 09:08:35 +01:00
Emmanuel Engelhart
8fdaa5f4db Bump-up to actions/checkout@v4 2024-12-20 08:31:44 +01:00
Emmanuel Engelhart
297627fbc7 Bump-up to actions/upload-artifact@v4 2024-12-20 08:31:44 +01:00
Emmanuel Engelhart
a3708c68ce Replace deprecated 'set-output' in workflows 2024-12-20 08:31:43 +01:00
Kelson
a809c671fd Merge pull request #1167 from kiwix/better-deb-control-version-checking
Check versus minor version of dependencies
2024-12-20 08:29:03 +01:00
Emmanuel Engelhart
31477bc99b Check versus minor version of dependencies 2024-12-20 08:19:45 +01:00
Kelson
6c37e2827e Merge pull request #1165 from kiwix/kelson42-patch-1
Stop building Windows with DEBUG symbols in CI
2024-11-26 08:37:20 +00:00
Kelson
9138f91c31 Stop building Windows with DEBUG symbols in CI 2024-11-26 09:35:55 +01:00
rgaudin
9bd568fe0e Merge pull request #1160 from kiwix/1157_fixmagnet
magnetLink queryString to start with ? and not ?&
2024-11-08 16:27:46 +00:00
rgaudin
eca7cf86e6 fixup! Fixed #1157: magnetLink queryString to start with ? and not ?& 2024-11-08 15:56:58 +00:00
rgaudin
77f4fd7447 Fixed #1157: magnetLink queryString to start with ? and not ?& 2024-11-08 14:58:28 +00:00
Kelson
84ebee899c Merge pull request #1154 from kiwix/translatewiki
Localisation updates from https://translatewiki.net.
2024-10-31 11:37:34 +01:00
translatewiki.net
9eed5da3be Localisation updates from https://translatewiki.net. 2024-10-28 13:07:08 +01:00
Kelson
20abebd623 Merge pull request #1152 from kiwix/macos13
removed temp hack
2024-10-09 17:18:52 +00:00
rgaudin
58a1af85b9 removed temp hack and unlink 2024-10-09 14:38:25 +00:00
Kelson
585f55d885 Merge pull request #1151 from kiwix/feature/fix-server-set-addr
Fix Server::setAddress
2024-10-09 14:24:46 +00:00
sgourdas
8b00c2eb22 Fix Server::setAddress 2024-10-09 12:17:01 +03:00
Kelson
c8bddd6cf4 Merge pull request #1149 from kiwix/feature/remove-ip-prefix
Remove 169.254 prefix
2024-10-09 05:25:16 +00:00
sgourdas
5d1b6274a8 Remove 169.254 prefix 2024-10-08 21:39:12 +03:00
Kelson
0de9bd0a99 Merge pull request #1141 from kiwix/translatewiki
Localisation updates from https://translatewiki.net.
2024-10-08 16:15:40 +00:00
translatewiki.net
b62274efdd Localisation updates from https://translatewiki.net. 2024-10-07 14:06:39 +02:00
Kelson
4a1498d8df Merge pull request #1145 from kiwix/ci_fix
Fix broken CI on Linux (aarch64, focal)
2024-10-03 16:48:21 +00:00
Veloman Yunkan
f6df2342cf Fix broken CI on Linux (aarch64, focal) 2024-10-03 17:13:34 +04:00
Kelson
8bbda99cab Merge pull request #1132 from kiwix/feature/best-public-ip
Refactor getBestPublicIp for all valid ips
2024-10-02 19:31:17 +00:00
sgourdas
95529d2c0a Add ip availability check in server start 2024-10-02 20:55:13 +03:00
sgourdas
b80699916d Refactor getBestPublicIp for both protocols 2024-10-02 20:55:11 +03:00
sgourdas
534916929d Line cleanup 2024-09-30 19:39:40 +03:00
sgourdas
02ab2ce5a5 Make server getters const 2024-09-30 19:39:40 +03:00
sgourdas
8930095c52 Make IpMode members uppercase 2024-09-30 19:39:40 +03:00
Kelson
bef3ec7694 Merge pull request #1140 from kiwix/release-14.0.0
libkiwix 14.0.0 Changelog
2024-09-28 14:36:36 +00:00
Emmanuel Engelhart
9057686a25 libkiwix 14.0.0 Changelog 2024-09-28 14:29:07 +00:00
Kelson
723dd977fe Merge pull request #1126 from kiwix/translatewiki
Localisation updates from https://translatewiki.net.
2024-09-28 14:28:07 +00:00
translatewiki.net
0b87d4fe04 Localisation updates from https://translatewiki.net. 2024-09-26 14:07:55 +02:00
Kelson
ea31e2f42f Merge pull request #1137 from kiwix/catalog_search_without_boolean_operators
Catalog search without support for Xapian boolean operators
2024-09-25 15:16:53 +00:00
Veloman Yunkan
01bda6b2c0 Disabled Xapian boolean operators in catalog query 2024-09-25 17:42:50 +04:00
Veloman Yunkan
de64a5a724 Testing of Xapian query operators in catalog search 2024-09-25 17:42:50 +04:00
Veloman Yunkan
90dd1cb3f0 Enhanced test book descriptions with xapian keywords
Added to test book descriptions words that serve as keywords
for query syntax with boolean operators (or, and, not, xor, near, adj) enabled.

Note that the change in indexed text has lead to the change in the order
of returned results.
2024-09-25 16:41:38 +04:00
Kelson
0b14fda94d Merge pull request #1135 from kiwix/fix-deb-package-deps
Fix deb package dependencies
2024-09-18 12:39:34 +00:00
Emmanuel Engelhart
fe965faf1b Fix deb package dependencies 2024-09-18 14:21:42 +02:00
Kelson
6ad1776242 Merge pull request #1133 from kiwix/pkgconfig
Generation of libkiwix.pc via meson's pkgconfig module
2024-09-17 10:35:29 +02:00
Veloman Yunkan
cbfd3ec7c4 Dropped generation of kiwix.pc
Below are the contents of libkiwix.pc and kiwix.pc files generated using
the new and old approaches, respectively, for native_dyn and
native_static configurations:

```
$ cat BUILD_native_dyn/INSTALL/lib/x86_64-linux-gnu/pkgconfig/libkiwix.pc
prefix=<REDACTED>
includedir=${prefix}/include
libdir=${prefix}/lib/x86_64-linux-gnu

Name: libkiwix
Description: A library that contains useful primitives that Kiwix readers have in common
Version: 14.0.0
Requires.private: icu-i18n, libzim < 10.0.0, libzim >= 9.0.0, pugixml, libcurl, libmicrohttpd, zlib, xapian-core
Libs: -L${prefix}/lib/x86_64-linux-gnu -lkiwix
Libs.private: -pthread
Cflags: -I${includedir}

$ cat BUILD_native_dyn/INSTALL/lib/x86_64-linux-gnu/pkgconfig/kiwix.pc
prefix=<REDACTED>
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: 14.0.0
Requires: libzim icu-i18n pugixml libcurl libmicrohttpd xapian-core
Libs: -L${libdir} -lkiwix
Cflags: -I${includedir}/

$ cat BUILD_native_static/INSTALL/lib/x86_64-linux-gnu/pkgconfig/libkiwix.pc
prefix=<REDACTED>
includedir=${prefix}/include
libdir=${prefix}/lib/x86_64-linux-gnu

Name: libkiwix
Description: A library that contains useful primitives that Kiwix readers have in common
Version: 14.0.0
Requires: icu-i18n, libzim < 10.0.0, libzim >= 9.0.0, pugixml, libcurl, libmicrohttpd, zlib, xapian-core
Libs: -L${prefix}/lib/x86_64-linux-gnu -lkiwix -pthread
Cflags: -I${includedir} -pthread

$ cat BUILD_native_static/INSTALL/lib/x86_64-linux-gnu/pkgconfig/kiwix.pc
prefix=<REDACTED>
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: 14.0.0
Requires: libzim icu-i18n pugixml libcurl libmicrohttpd xapian-core
Libs: -L${libdir} -lkiwix
Cflags: -I${includedir}/
```

The notable differences are:

- libdir changed from `${prefix}/lib64` to `${prefix}/lib/x86_64-linux-gnu`

- for native_dyn configuration Requires.private is used

- pthread has appeared in Libs/Libs.private and/or Cflags

- version information was added to the libzim requirement
2024-09-17 12:26:14 +04:00
Veloman Yunkan
f6765137e7 Fixed the libzim version requirement
Also fixed capitalization in an error message
2024-09-16 17:26:28 +04:00
Veloman Yunkan
c24e04c8da Generation of libkiwix.pc via meson's pkgconfig module
Generation of kiwix.pc using the previous approach still stays in place.
2024-09-16 17:25:42 +04:00
Kelson
327fec1877 Merge pull request #1131 from kiwix/ungarbled_binary_resources
Ungarbled binary resources
2024-09-14 14:43:23 +02:00
Veloman Yunkan
c8524b95bc Protection against adding resources of new types
Now if a static resource of a new type is added the build will
fail unless the list of known file type extensions is updated.
2024-09-12 17:39:49 +04:00
Veloman Yunkan
0ac3130b0d Added an extension to an extensionless resource
... so that all resources have extensions and can be automatically
categorized as binary or text based on extension (coming next).
2024-09-12 17:18:16 +04:00
Veloman Yunkan
425ae1efae Disabled dos2unix conversion for binary resources 2024-09-11 17:52:11 +04:00
Veloman Yunkan
920d603a89 Validation of resource content against cacheid
Added a test that checks that the static resources returned by the
server have content that matches their cacheid. This test currently
fails because some binary resources (e.g. png images) are garbled by the
dos2unix conversion.
2024-09-11 17:46:40 +04:00
Kelson
f5c91cc272 Merge pull request #1094 from kiwix/downloadBtn
More conspicuous download button
2024-09-11 11:42:59 +02:00
Veloman Yunkan
3cdc036858 Updated nojs library page
... to keep it in sync with the JSful library page that has been
modified in the previous commit.
2024-09-11 13:17:27 +04:00
Nikhil Tanwar
29bfaa5c5b Move download button to end of tile
- Changed the position of download button to the end of tile and
  added a proper download icon to it. When the button is hovered it
  becomes darker.

- Also internationalized the "Download" text on the modal download widget
  and added download size information to it.
2024-09-11 11:02:08 +04:00
Kelson
bec80e8091 Merge pull request #1123 from kiwix/handling_of_external_app_links
Handling of external app links in the viewer
2024-09-10 13:45:50 +00:00
Veloman Yunkan
2da9801bac Fixed ctrl-clicking of links for Zimit2 ZIMs
Zimit2 ZIMs employ Wombat for client-side rewriting of URLs. The latter
interferes with our approach of handling in the viewer CTRL/SHIFT-clicks
on links inside articles. This commit disables Wombat temporarily while
changing the href attribute of the clicked link.
2024-09-10 17:03:04 +04:00
Veloman Yunkan
16ebc6611b Handling of external app links in the viewer
Links that should be handled/opened by external applications - such as email
addresses (mailto:), phone numbers (tel:), etc - are opened by the
viewer in a new tab/window, thus avoiding any issues with content
security policy.
2024-09-10 16:28:28 +04:00
Kelson
d5a44b913e Merge pull request #1127 from kiwix/crisp_illustration_on_book_tile
Illustration size on book tile is fixed to 48x48
2024-09-09 18:03:57 +00:00
Veloman Yunkan
a63a162c58 Illustration size on book tile is fixed to 48x48 2024-09-09 17:47:55 +00:00
Kelson
c29cd8cf3b Merge pull request #1128 from kiwix/mul_as_mul
Deprived pseudolang mul of a self-name
2024-09-09 15:03:47 +00:00
Veloman Yunkan
04bf1be9d6 Deprived pseudolang mul of a self-name
ICU package contains a special code "mul" with a self-name of "multiple
languages". libkiwix now suppresses that. As a result the self-name of
"mul" (like for any other unknown language code) is the code itself
(i.e. "mul").

The most prominent user-visible effect of this change is that the
language filter in the library page no longer contains a "Multiple
languages" entry if there is a legacy ZIM file with the language set to
"mul" - that entry now shows up as "Mul".
2024-09-09 16:57:07 +04:00
Kelson
59054aa5ad Merge pull request #1109 from kiwix/translatewiki
Localisation updates from https://translatewiki.net.
2024-09-08 20:49:31 +00:00
translatewiki.net
1b8dde0115 Localisation updates from https://translatewiki.net. 2024-09-08 19:34:42 +00:00
Kelson
9d0f6a3170 Merge pull request #1125 from kiwix/remove-windows-cross-compile
Remove Windows cross-compilation from CI
2024-09-08 19:34:27 +00:00
Emmanuel Engelhart
c16ed0aa4c Remove Windows cross-compilation from CI 2024-09-08 21:18:16 +02:00
Kelson
3d95b386c6 Merge pull request #1124 from kiwix/better-libzim-version-check
Better check libzim version
2024-09-07 13:43:37 +00:00
Emmanuel Engelhart
a3f5a654f2 Better check libzim version 2024-09-07 15:35:30 +02:00
Kelson
801b1df304 Merge pull request #1121 from kiwix/safe_tags_without_makeup
Untransformed (but HTML-safe) tags in the kiwix-serve frontend
2024-09-05 04:43:29 +00:00
Veloman Yunkan
2b8a071c6f Fixed cacheids in unit-tests 2024-09-04 19:13:41 +04:00
Veloman Yunkan
00fae37f2d Fixed vertical alignment of the tag filter indicator 2024-09-04 19:09:35 +04:00
Veloman Yunkan
846404e959 Proper HTML encoding/decoding of tags in the frontend
- Tags in the OPDS feed are HTML encoded and must be decoded.

- Tag values must be HTML encoded when injected into the DOM:

  * When the injection is done by setting the innerHTML attribute of a
    DOM element, HTML encoding must be done explicitly (since that text
    is going to be parsed as HTML).

  * When the tag value is expanded into a string that is then set as an
    attribute of a DOM element via the setAttribute() method, no HTML
    encoding must be done (since Element.setAttribute() directly sets
    that value and no HTML decoding is involved in that operation).
2024-09-04 18:43:24 +04:00
Veloman Yunkan
fbcd160efd Disabled beautification of tags in the frontend 2024-09-04 18:15:01 +04:00
Kelson
196185dd73 Merge pull request #1119 from kiwix/fix-debian-control-file-for-v14
Fix deb control file for 14.0.0
2024-09-02 18:14:23 +00:00
Emmanuel Engelhart
affb996769 Fix deb control file for 14.0.0 2024-09-02 19:59:27 +02:00
Kelson
418abbcefa Merge pull request #1116 from kiwix/bump-up-to-version-14.0.0
Bump-up version to 14.0.0
2024-09-01 12:47:13 +00:00
Emmanuel Engelhart
00867a13f6 Rename deb .install file 2024-09-01 14:39:45 +02:00
Emmanuel Engelhart
e096c7e2fd Bump-up version to 14.0.0 2024-09-01 10:48:49 +02:00
Kelson
69341eab47 Merge pull request #1114 from kiwix/update-deb-packages-ci-cd
Update deb packages CI/CD
2024-09-01 08:41:30 +00:00
Emmanuel Engelhart
082727ebb6 Comment out Debian related CI/CD 2024-09-01 10:31:19 +02:00
Emmanuel Engelhart
75a4f8b806 Requires libzim9 2024-09-01 10:14:03 +02:00
Emmanuel Engelhart
2eaa1c4649 Refresh deb packages CI/CD 2024-09-01 10:06:31 +02:00
Matthieu Gautier
199a10d093 Merge pull request #1113 from kiwix/ci_windows 2024-08-27 13:25:40 +02:00
Matthieu Gautier
4812fb18f6 Ensure resources use \n as newline and not \r\n
It appears that git on Windows replace `\n` with `\r\n` in the working
tree.

So compiled resources contain a extra `\r` and our tests are failing.
2024-08-27 13:17:53 +02:00
Matthieu Gautier
940818d801 Do not do iterator underflow in urlDecode
`value.end() - 3` may be "before start of string" if string length is < 3.
On Windows debug, is throw an exception.
On other platform it is probably an undefined behavior.

Rewrite the test to avoid such invalid substraction.
2024-08-27 13:17:53 +02:00
Matthieu Gautier
5182a66b19 Update test to have Windows paths on Windows 2024-08-27 13:17:53 +02:00
Matthieu Gautier
b688aa294a [CI] Build libkiwix on Windows CI
As kiwix-build now publish deps archive for libkiwix on Windows (github actions),
we can now start compiling (and test !!) libkiwix on Windows CI.
2024-08-22 17:04:54 +02:00
Kelson
27ad77c566 Merge pull request #1111 from kiwix/revert-1110-revert-1107-feature/data-directory
"Removed getDataDirectory()" again
2024-08-21 23:50:24 +02:00
Matthieu Gautier
7677f76854 Revert "Revert "Removed getDataDirectory()"" 2024-08-21 22:58:42 +02:00
Kelson
513a8d1383 Merge pull request #1105 from kiwix/string_slugification
Add String Slugification for Generating File Name
2024-08-14 20:24:08 +00:00
ShaopengLin
be464a5986 Introduce getSlugifiedFileName in tools.h
The function sanitizes file names depending on OS.
2024-08-14 20:11:11 +00:00
Kelson
c2042c3be8 Merge pull request #1112 from kiwix/public_i18n_api
Public i18n API
2024-08-14 11:18:29 +00:00
Veloman Yunkan
8d480c8b6d Introduced translateBookCategory() 2024-08-14 12:45:37 +04:00
Veloman Yunkan
82ff88f5d8 Made some i18n API public 2024-08-14 12:45:37 +04:00
Veloman Yunkan
2535f210b3 Renamed src/server/{i18n -> i18n_utils}.h
... so that i18n.h can be introduced in include/
2024-08-14 12:44:20 +04:00
Matthieu Gautier
cb0a2c234a Merge pull request #1110 from kiwix/revert-1107-feature/data-directory 2024-08-12 16:32:21 +02:00
Matthieu Gautier
5a73a75798 Revert "Removed getDataDirectory()" 2024-08-12 16:22:01 +02:00
Matthieu Gautier
0ea756c42a Merge pull request #1107 from kiwix/feature/data-directory 2024-08-12 14:41:33 +02:00
sgourdas
7108dfa9c2 Remove makeDirectory 2024-08-11 23:46:48 +03:00
sgourdas
9fd8e81de2 Remove getDataDirectory 2024-08-11 23:46:34 +03:00
sgourdas
566b40a2f8 Pass download directory directly to startDownload 2024-08-11 23:45:27 +03:00
Kelson
ff6d8a4b30 Merge pull request #1108 from kiwix/pkgconf
Replace pkg-config by pkgconf package in deb
2024-08-07 05:03:38 +00:00
Emmanuel Engelhart
f456ce3e93 Replace pkg-config by pkgconf 2024-08-07 06:53:25 +02:00
101 changed files with 2591 additions and 770 deletions

View File

@@ -33,17 +33,12 @@ jobs:
HOME: /Users/runner
steps:
- name: Retrieve source code
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install packages
run: |
brew update
brew unlink python3
# upgrade from python@3.12 to python@3.12.2 fails to overwrite those
rm -f /usr/local/bin/2to3 /usr/local/bin/2to3-3.12 /usr/local/bin/idle3 /usr/local/bin/idle3.12 /usr/local/bin/pydoc3 /usr/local/bin/pydoc3.12 /usr/local/bin/python3 /usr/local/bin/python3-config /usr/local/bin/python3.12 /usr/local/bin/python3.12-config
brew install pkg-config ninja meson
env:
HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: 1
brew install ninja meson
- name: Install dependencies
uses: kiwix/kiwix-build/actions/dl_deps_archive@main
@@ -71,6 +66,52 @@ jobs:
LD_LIBRARY_PATH: ${{env.HOME}}/BUILD_${{matrix.arch_name}}/INSTALL/lib:${{env.HOME}}/BUILD_${{matrix.arch_name}}/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
@@ -80,8 +121,6 @@ jobs:
- linux-x86_64-dyn
- android-arm
- android-arm64
- win32-static
- win32-dyn
include:
- target: linux-x86_64-static
image_variant: focal
@@ -107,26 +146,14 @@ jobs:
arch_name: aarch64-linux-android
run_test: false
coverage: false
- target: win32-static
image_variant: f35
lib_postfix: '64'
arch_name: i686-w64-mingw32
run_test: false
coverage: false
- target: win32-dyn
image_variant: f35
lib_postfix: '64'
arch_name: i686-w64-mingw32
run_test: false
coverage: false
env:
HOME: /home/runner
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
container:
image: "ghcr.io/kiwix/kiwix-build_ci_${{matrix.image_variant}}:38"
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install dependencies
uses: kiwix/kiwix-build/actions/dl_deps_archive@main
with:

View File

@@ -15,11 +15,16 @@ jobs:
fail-fast: false
matrix:
distro:
- debian-unstable
# - debian-unstable
# - debian-trixie
# - debian-bookworm
# - debian-bullseye
- ubuntu-noble
- ubuntu-jammy
- ubuntu-focal
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
# Determine which PPA we should upload to
- name: PPA
@@ -27,25 +32,54 @@ jobs:
run: |
if [[ $REF == refs/tags* ]]
then
echo "::set-output name=ppa::kiwixteam/release"
echo "ppa=kiwixteam/release" >> $GITHUB_OUTPUT
else
echo "::set-output name=ppa::kiwixteam/dev"
echo "ppa=kiwixteam/dev" >> $GITHUB_OUTPUT
fi
env:
REF: ${{ github.ref }}
- uses: legoktm/gh-action-auto-dch@master
- uses: legoktm/gh-action-auto-dch@main
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
# - 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
with:
args: --no-sign
ppa: ${{ steps.ppa.outputs.ppa }}
- uses: legoktm/gh-action-build-deb@ubuntu-jammy
if: matrix.distro == 'ubuntu-jammy'
@@ -63,12 +97,12 @@ jobs:
args: --no-sign
ppa: ${{ steps.ppa.outputs.ppa }}
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
with:
name: Packages for ${{ matrix.distro }}
path: output
- uses: legoktm/gh-action-dput@master
- uses: legoktm/gh-action-dput@main
name: Upload dev package
# Only upload on pushes to main
if: github.event_name == 'push' && github.event.ref == 'refs/heads/main' && startswith(matrix.distro, 'ubuntu-')
@@ -77,7 +111,7 @@ jobs:
repository: ppa:kiwixteam/dev
packages: output/*_source.changes
- uses: legoktm/gh-action-dput@master
- uses: legoktm/gh-action-dput@main
name: Upload release package
if: github.event_name == 'release' && startswith(matrix.distro, 'ubuntu-')
with:

View File

@@ -1,3 +1,31 @@
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
===============

17
debian/control vendored
View File

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

@@ -16,7 +16,7 @@
namespace kiwix {
enum class IpMode { ipv4, ipv6, all };
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

@@ -172,7 +172,12 @@ class Downloader
typedef std::vector<std::pair<std::string, std::string>> Options;
public: // functions
Downloader();
/*
* Create a new Downloader object.
*
* @param sessionFileDir: The directory where aria2 will store its session file.
*/
explicit Downloader(std::string sessionFileDir);
virtual ~Downloader();
void close();
@@ -191,10 +196,11 @@ class Downloader
* 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 Options& options = {});
std::shared_ptr<Download> startDownload(const std::string& uri, const std::string& downloadDir, Options options = {});
/**
* Get a download corrsponding to a download id (did)

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2022 Veloman Yunkan <veloman.yunkan@gmail.com>
* 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
@@ -17,29 +17,16 @@
* MA 02110-1301, USA.
*/
#ifndef KIWIX_SERVER_I18N
#define KIWIX_SERVER_I18N
#ifndef KIWIX_I18N
#define KIWIX_I18N
#include <tools.h>
#include <map>
#include <string>
#include <mustache.hpp>
namespace kiwix
{
struct I18nString {
const char* const key;
const char* const value;
};
struct I18nStringTable {
const char* const lang;
const size_t entryCount;
const I18nString* const entries;
const char* get(const std::string& key) const;
};
std::string getTranslatedString(const std::string& lang, const std::string& key);
namespace i18n
{
@@ -69,28 +56,6 @@ private:
const std::string m_lang;
};
class GetTranslatedStringWithMsgId
{
typedef kainjow::mustache::basic_data<std::string> MustacheString;
typedef std::pair<std::string, MustacheString> MsgIdAndTranslation;
public:
explicit GetTranslatedStringWithMsgId(const std::string& lang) : m_lang(lang) {}
MsgIdAndTranslation operator()(const std::string& key) const
{
return {key, getTranslatedString(m_lang, key)};
}
MsgIdAndTranslation operator()(const std::string& key, const Parameters& params) const
{
return {key, expandParameterizedString(m_lang, key, params)};
}
private:
const std::string m_lang;
};
} // namespace i18n
class ParameterizedMessage
@@ -120,18 +85,8 @@ inline ParameterizedMessage nonParameterizedMessage(const std::string& msgId)
return ParameterizedMessage(msgId, noParams);
}
struct LangPreference
{
const std::string lang;
const float preference;
};
typedef std::vector<LangPreference> UserLangPreferences;
UserLangPreferences parseUserLanguagePreferences(const std::string& s);
std::string selectMostSuitableLanguage(const UserLangPreferences& prefs);
std::string translateBookCategory(const std::string& lang, const std::string& category);
} // namespace kiwix
#endif // KIWIX_SERVER_I18N
#endif // KIWIX_I18N

View File

@@ -10,7 +10,8 @@ headers = [
'kiwixserve.h',
'name_mapper.h',
'tools.h',
'version.h'
'version.h',
'i18n.h'
]
install_headers(headers, subdir:'kiwix')

View File

@@ -22,7 +22,7 @@
#include <string>
#include <memory>
#include "common.h"
#include "tools.h"
namespace kiwix
{
@@ -52,7 +52,7 @@ namespace kiwix
void stop();
void setRoot(const std::string& root);
void setAddress(const std::string& addr) { m_addr = addr; }
void setAddress(const std::string& addr);
void setPort(int port) { m_port = port; }
void setNbThreads(int threads) { m_nbThreads = threads; }
void setMultiZimSearchLimit(unsigned int limit) { m_multizimSearchLimit = limit; }
@@ -64,15 +64,15 @@ namespace kiwix
void setBlockExternalLinks(bool blockExternalLinks)
{ m_blockExternalLinks = blockExternalLinks; }
void setIpMode(IpMode mode) { m_ipMode = mode; }
int getPort();
std::string getAddress();
int getPort() const;
IpAddress getAddress() const;
IpMode getIpMode() const;
protected:
std::shared_ptr<Library> mp_library;
std::shared_ptr<NameMapper> mp_nameMapper;
std::string m_root = "";
std::string m_addr = "";
IpAddress m_addr;
std::string m_indexTemplateString = "";
int m_port = 80;
int m_nbThreads = 1;
@@ -81,7 +81,7 @@ namespace kiwix
bool m_withTaskbar = true;
bool m_withLibraryButton = true;
bool m_blockExternalLinks = false;
IpMode m_ipMode = IpMode::ipv4;
IpMode m_ipMode = IpMode::AUTO;
int m_ipConnectionLimit = 0;
std::unique_ptr<InternalServer> mp_server;
};

View File

@@ -24,6 +24,7 @@
#include <vector>
#include <map>
#include <cstdint>
#include "common.h"
namespace kiwix
{
@@ -45,32 +46,6 @@ typedef std::vector<std::string> FeedCategories;
*/
std::string getCurrentDirectory();
/**
* Return the data directory
*
* The data directory is the default directory where downloaded files
* should be saved (it can be overriden via the options parameter of
* `kiwix::Downloader::startDownload()`).
*
* Its path can vary and is determined as follows:
*
* * `$KIWIX_DATA_DIR` if `$KIWIX_DATA_DIR` environment variable set, *otherwise...*
* * On Windows:
*
* * `$APPDATA/kiwix` if environment variable `$APPDATA` set, *otherwise...*
* * `$USERPROFILE/kiwix` if environment variable `$USERPROFILE` set, *otherwise...*
*
* * On other Operating Systems:
*
* * `$XDG_DATA_HOME/kiwix` if environment variable `$XDG_DATA_HOME` set, *otherwise...*
* * `$HOME/.local/share/kiwx` if environment variable `$HOME` set, *otherwise...*
*
* * Current working directory.
*
* @return the path of the data directory (UTF-8 encoded)
*/
std::string getDataDirectory();
/** Return the path of the executable
*
* Some application may be packaged in auto extractible archive (Appimage) and the
@@ -240,9 +215,10 @@ std::map<std::string, IpAddress> getNetworkInterfacesIPv4Or6();
std::map<std::string, std::string> getNetworkInterfaces();
/** Provides the best IP address
* This function provides the best IP address from the list given by getNetworkInterfacesIPv4Or6()
* This function provides the best IP addresses for both ipv4 and ipv6 protocols,
* in an IpAddress struct, based on the list given by getNetworkInterfacesIPv4Or6()
*/
std::string getBestPublicIp(bool ipv6);
IpAddress getBestPublicIps();
/** Provides the best IPv4 adddress
* Equivalent to getBestPublicIp(false). Provided for backward compatibility
@@ -284,12 +260,12 @@ FeedCategories readCategoriesFromFeed(const std::string& content);
std::string getLanguageSelfName(const std::string& lang);
/**
* Retrieve the translation corresponding to key in language 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 lang ISO 639-3 language code.
* @param key translation key string
* @return translated key
* @param filename Valid UTF-8 encoded file name string.
* @return slugified string.
*/
std::string getTranslatedString(const std::string& lang, const std::string& key);
std::string getSlugifiedFileName(const std::string& filename);
}
#endif // KIWIX_TOOLS_H

View File

@@ -1,10 +0,0 @@
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,21 +1,49 @@
project('libkiwix', 'cpp',
version : '13.1.0',
version : '14.0.0',
license : 'GPLv3+',
default_options : ['c_std=c11', 'cpp_std=c++17', 'werror=true'])
compiler = meson.get_compiler('cpp')
static_deps = get_option('static-linkage') or get_option('default_library') == 'static'
extra_libs = []
# 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 = []
# 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']
endif
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
# 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'
thread_dep = dependency('threads')
else
thread_dep = dependency('', required:false)
@@ -35,9 +63,10 @@ else
error('Cannot found header mustache.hpp')
endif
libzim_dep = dependency('libzim', version : '>=8.1.0', static:static_deps)
libzim_dep = dependency('libzim', version:['>=9.0.0', '<10.0.0'], static:static_deps)
if not compiler.has_header_symbol('zim/zim.h', 'LIBZIM_WITH_XAPIAN', dependencies: libzim_dep)
error('Libzim seems to be compiled without xapian. Xapian support is mandatory.')
error('Libzim seems to be compiled without Xapian. Xapian support is mandatory.')
endif
@@ -73,17 +102,10 @@ if get_option('doc')
subdir('docs')
endif
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'
)
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)

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.h"
#include "server/i18n_utils.h"
namespace kiwix {
namespace i18n {

View File

@@ -61,6 +61,32 @@ 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
@@ -72,6 +98,8 @@ 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

@@ -55,18 +55,15 @@ void pauseAnyActiveDownloads(const std::string& ariaSessionFilePath)
} // unnamed namespace
Aria2::Aria2():
Aria2::Aria2(std::string sessionFileDir):
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 download_dir = "--dir=" + getDataDirectory();
std::string session_file = appendToDirectory(getDataDirectory(), "kiwix.session");
std::string session_file = appendToDirectory(sessionFileDir, "kiwix.session");
pauseAnyActiveDownloads(session_file);
std::string session = "--save-session=" + session_file;
std::string inputFile = "--input-file=" + session_file;
@@ -94,7 +91,6 @@ Aria2::Aria2():
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());
}
@@ -128,7 +124,7 @@ Aria2::Aria2():
typedef std::chrono::duration<double> Seconds;
const double MAX_WAITING_TIME_SECONDS = 0.5;
const double MAX_WAITING_TIME_SECONDS = 1;
const auto t0 = std::chrono::steady_clock::now();
bool maxWaitingTimeWasExceeded = false;

View File

@@ -22,11 +22,10 @@ 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:
Aria2();
explicit Aria2(std::string sessionFileDir);
virtual ~Aria2() = default;
void close();

View File

@@ -125,8 +125,8 @@ void Download::cancelDownload()
}
/* Constructor */
Downloader::Downloader() :
mp_aria(new Aria2())
Downloader::Downloader(std::string sessionFileDir) :
mp_aria(new Aria2(sessionFileDir))
{
try {
for (auto gid : mp_aria->tellWaiting()) {
@@ -209,9 +209,13 @@ bool downloadCanBeReused(const Download& d,
} // unnamed namespace
std::shared_ptr<Download> Downloader::startDownload(const std::string& uri, const Options& options)
std::shared_ptr<Download> Downloader::startDownload(const std::string& uri, const std::string& downloadDir, Options 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) )

View File

@@ -3,7 +3,7 @@
#include "tools/otherTools.h"
#include "tools.h"
#include "tools/regexTools.h"
#include "server/i18n.h"
#include "server/i18n_utils.h"
namespace kiwix
{
@@ -27,6 +27,30 @@ 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);
@@ -72,17 +96,16 @@ 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},
{"langCode", langCode},
{"langTag", langTagObj},
{"faviconAttr", faviconAttr},
{"tagList", getTagList(tags)},
{"downloadAvailable", downloadAvailable}

View File

@@ -645,8 +645,6 @@ 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;

View File

@@ -32,7 +32,7 @@
#include "libkiwix-resources.h"
#include "tools/stringTools.h"
#include "server/i18n.h"
#include "server/i18n_utils.h"
namespace kiwix
{

View File

@@ -75,12 +75,26 @@ void Server::setRoot(const std::string& root)
}
}
int Server::getPort()
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;
}
}
int Server::getPort() const
{
return mp_server->getPort();
}
std::string Server::getAddress()
IpAddress Server::getAddress() const
{
return mp_server->getAddress();
}

View File

@@ -17,7 +17,7 @@
* MA 02110-1301, USA.
*/
#include "i18n.h"
#include "i18n_utils.h"
#include "tools/otherTools.h"
@@ -193,4 +193,13 @@ 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

83
src/server/i18n_utils.h Normal file
View File

@@ -0,0 +1,83 @@
/*
* Copyright 2022 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_SERVER_I18N_UTILS
#define KIWIX_SERVER_I18N_UTILS
#include "i18n.h"
#include <mustache.hpp>
namespace kiwix
{
struct I18nString {
const char* const key;
const char* const value;
};
struct I18nStringTable {
const char* const lang;
const size_t entryCount;
const I18nString* const entries;
const char* get(const std::string& key) const;
};
namespace i18n
{
class GetTranslatedStringWithMsgId
{
typedef kainjow::mustache::basic_data<std::string> MustacheString;
typedef std::pair<std::string, MustacheString> MsgIdAndTranslation;
public:
explicit GetTranslatedStringWithMsgId(const std::string& lang) : m_lang(lang) {}
MsgIdAndTranslation operator()(const std::string& key) const
{
return {key, getTranslatedString(m_lang, key)};
}
MsgIdAndTranslation operator()(const std::string& key, const Parameters& params) const
{
return {key, expandParameterizedString(m_lang, key, params)};
}
private:
const std::string m_lang;
};
} // namespace i18n
struct LangPreference
{
const std::string lang;
const float preference;
};
typedef std::vector<LangPreference> UserLangPreferences;
UserLangPreferences parseUserLanguagePreferences(const std::string& s);
std::string selectMostSuitableLanguage(const UserLangPreferences& prefs);
} // namespace kiwix
#endif // KIWIX_SERVER_I18N_UTILS

View File

@@ -54,7 +54,7 @@ extern "C" {
#include "search_renderer.h"
#include "opds_dumper.h"
#include "html_dumper.h"
#include "i18n.h"
#include "i18n_utils.h"
#include <zim/uuid.h>
#include <zim/error.h>
@@ -85,6 +85,20 @@ namespace kiwix {
namespace
{
bool ipAvailable(const std::string addr)
{
auto interfaces = kiwix::getNetworkInterfacesIPv4Or6();
for (const auto& kv : interfaces) {
const auto& interfaceIps = kv.second;
if ((interfaceIps.addr == addr) || (interfaceIps.addr6 == addr)) {
return true;
}
}
return false;
}
inline std::string normalizeRootUrl(std::string rootUrl)
{
while ( !rootUrl.empty() && rootUrl.back() == '/' )
@@ -407,7 +421,7 @@ public:
InternalServer::InternalServer(LibraryPtr library,
std::shared_ptr<NameMapper> nameMapper,
std::string addr,
IpAddress addr,
int port,
std::string root,
int nbThreads,
@@ -461,30 +475,44 @@ bool InternalServer::start() {
sockAddr6.sin6_family = AF_INET6;
sockAddr6.sin6_port = htons(m_port);
if (m_addr.empty()) {
if (0 != INADDR_ANY) {
sockAddr6.sin6_addr = in6addr_any;
sockAddr4.sin_addr.s_addr = htonl(INADDR_ANY);
}
m_addr = kiwix::getBestPublicIp(m_ipMode == IpMode::ipv6 || m_ipMode == IpMode::all);
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;
} else {
bool ipv6 = inet_pton(AF_INET6, m_addr.c_str(), &(sockAddr6.sin6_addr.s6_addr)) == 1;
bool ipv4 = inet_pton(AF_INET, m_addr.c_str(), &(sockAddr4.sin_addr.s_addr)) == 1;
if (ipv6){
m_ipMode = IpMode::all;
} else if (!ipv4) {
std::cerr << "Ip address " << m_addr << " is not a valid ip address" << std::endl;
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;
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) {
if (m_ipMode == IpMode::ALL) {
flags|=MHD_USE_DUAL_STACK;
} else if (m_ipMode == IpMode::ipv6) {
} else if (m_ipMode == IpMode::IPV6) {
flags|=MHD_USE_IPv6;
}
struct sockaddr* sockaddr = (m_ipMode==IpMode::all || m_ipMode==IpMode::ipv6)
struct sockaddr* sockaddr = (m_ipMode==IpMode::ALL || m_ipMode==IpMode::IPV6)
? (struct sockaddr*)&sockAddr6
: (struct sockaddr*)&sockAddr4;
@@ -747,6 +775,7 @@ std::unique_ptr<Response> InternalServer::handle_suggest(const RequestContext& r
}
const auto queryString = request.get_optional_param("term", std::string());
const auto mode = request.get_optional_param("mode", std::string());
const auto start = request.get_optional_param<unsigned int>("start", 0);
unsigned int count = request.get_optional_param<unsigned int>("count", 10);
if (count == 0) {
@@ -765,13 +794,17 @@ std::unique_ptr<Response> InternalServer::handle_suggest(const RequestContext& r
);
const auto lock(searcher->getLock());
auto search = searcher->suggest(queryString);
auto srs = search.getResults(start, count);
for(auto& suggestion: srs) {
results.add(suggestion);
if ( start == 0 && mode == "smart") {
for(const auto& suggestion: search.getSmartSuggestions(count)) {
results.add(suggestion);
}
} else {
for(const auto& suggestion: search.getResults(start, count)) {
results.add(suggestion);
}
}
/* Propose the fulltext search if possible */
if (archive->hasFulltextIndex()) {
results.addFTSearchSuggestion(request.get_user_language(), queryString);
@@ -959,7 +992,7 @@ 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);
const auto cssUrl = renderUrl(m_root, RESOURCE::templates::url_of_search_results_css_tmpl);
HTTPErrorResponse response(request, MHD_HTTP_NOT_FOUND,
"fulltext-search-unavailable",
"404-page-heading",
@@ -1057,9 +1090,7 @@ std::unique_ptr<Response> InternalServer::handle_captured_external(const Request
return UrlNotFoundResponse(request);
}
auto data = get_default_data();
data.set("source", source);
return ContentResponse::build(RESOURCE::templates::captured_external_html, data, "text/html; charset=utf-8");
return BlockExternalLinkResponse(request, m_root, source);
}
std::unique_ptr<Response> InternalServer::handle_catch(const RequestContext& request)
@@ -1093,15 +1124,6 @@ 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
@@ -1155,9 +1177,7 @@ std::unique_ptr<Response> InternalServer::handle_content(const RequestContext& r
} catch (const std::out_of_range& e) {}
if (archive == nullptr) {
const std::string searchURL = m_root + "/search?pattern=" + kiwix::urlEncode(pattern);
return UrlNotFoundResponse(request)
+ suggestSearchMsg(searchURL, kiwix::urlDecode(pattern));
return NewHTTP404Response(request, m_root, m_root + url);
}
const std::string archiveUuid(archive->getUuid());
@@ -1202,9 +1222,7 @@ std::unique_ptr<Response> InternalServer::handle_content(const RequestContext& r
if (m_verbose.load())
printf("Failed to find %s\n", urlStr.c_str());
std::string searchURL = m_root + "/search?content=" + bookName + "&pattern=" + kiwix::urlEncode(pattern);
return UrlNotFoundResponse(request)
+ suggestSearchMsg(searchURL, kiwix::urlDecode(pattern));
return NewHTTP404Response(request, m_root, m_root + url);
}
}

View File

@@ -27,6 +27,7 @@ extern "C" {
#include "library.h"
#include "name_mapper.h"
#include "tools.h"
#include <zim/search.h>
#include <zim/suggestion.h>
@@ -94,7 +95,7 @@ class InternalServer {
public:
InternalServer(LibraryPtr library,
std::shared_ptr<NameMapper> nameMapper,
std::string addr,
IpAddress addr,
int port,
std::string root,
int nbThreads,
@@ -117,8 +118,8 @@ class InternalServer {
void** cont_cls);
bool start();
void stop();
std::string getAddress() { return m_addr; }
int getPort() { return m_port; }
IpAddress getAddress() const { return m_addr; }
int getPort() const { return m_port; }
IpMode getIpMode() const { return m_ipMode; }
private: // functions
@@ -166,7 +167,7 @@ class InternalServer {
typedef ConcurrentCache<std::string, std::shared_ptr<LockableSuggestionSearcher>> SuggestionSearcherCache;
private: // data
std::string m_addr;
IpAddress m_addr;
int m_port;
std::string m_root; // URI-encoded
std::string m_rootPrefixOfDecodedURL; // URI-decoded

View File

@@ -28,7 +28,7 @@
#include <cctype>
#include "tools/stringTools.h"
#include "i18n.h"
#include "i18n_utils.h"
namespace kiwix {

View File

@@ -243,6 +243,23 @@ public:
};
}
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;
@@ -368,6 +385,45 @@ std::unique_ptr<ContentResponse> ContentResponseBlueprint::generateResponseObjec
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,
int httpStatusCode,
const std::string& pageTitleMsgId,
@@ -383,8 +439,8 @@ HTTPErrorResponse::HTTPErrorResponse(const RequestContext& request,
Data::List emptyList;
*this->m_data = Data(Data::Object{
{"CSS_URL", Data::onlyAsNonEmptyValue(cssUrl) },
{"PAGE_TITLE", Data::from(nonParameterizedMessage(pageTitleMsgId))},
{"PAGE_HEADING", Data::from(nonParameterizedMessage(headingMsgId))},
{"PAGE_TITLE", Data::fromMsgId(pageTitleMsgId)},
{"PAGE_HEADING", Data::fromMsgId(headingMsgId)},
{"details", emptyList}
});
}

View File

@@ -27,7 +27,7 @@
#include <mustache.hpp>
#include "byte_range.h"
#include "etag.h"
#include "i18n.h"
#include "i18n_utils.h"
#include <zim/item.h>
@@ -145,6 +145,13 @@ protected: //data
std::unique_ptr<Data> m_data;
};
struct NewHTTP404Response : ContentResponseBlueprint
{
NewHTTP404Response(const RequestContext& request,
const std::string& root,
const std::string& urlPath);
};
struct HTTPErrorResponse : ContentResponseBlueprint
{
HTTPErrorResponse(const RequestContext& request,
@@ -190,6 +197,13 @@ 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

@@ -68,6 +68,7 @@ void fillLanguagesMap()
const kiwix::ICULanguageInfo lang(*icuLangPtr);
iso639_3.insert({lang.iso3Code(), lang.selfName()});
}
iso639_3.erase("mul");
}
} // unnamed namespace

View File

@@ -19,6 +19,7 @@
*/
#include "tools.h"
#include "stringTools.h"
#include <tools/networkTools.h>
#include <stdio.h>
@@ -41,6 +42,7 @@
#include <arpa/inet.h>
#include <ifaddrs.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <net/if.h>
#include <netdb.h>
#endif
@@ -62,6 +64,12 @@ 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) {
@@ -85,7 +93,6 @@ std::string download(const std::string& url) {
return ss.str();
}
namespace
{
@@ -211,40 +218,36 @@ std::map<std::string, std::string> getNetworkInterfaces() {
return result;
}
std::string getBestPublicIp(bool ipv6) {
IpAddress bestPublicIp = IpAddress{"127.0.0.1","::1"};
IpAddress getBestPublicIps() {
IpAddress bestPublicIps;
std::map<std::string, IpAddress> interfaces = getNetworkInterfacesIPv4Or6();
#ifndef _WIN32
const char* const prioritizedNames[] =
{ "eth0", "eth1", "wlan0", "wlan1", "en0", "en1" };
for(auto name: prioritizedNames) {
auto it=interfaces.find(name);
if(it != interfaces.end() && !(ipv6 && (*it).second.addr6.empty())) {
bestPublicIp = (*it).second;
break;
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);
}
}
#endif
const char* const prefixes[] = { "192.168", "172.16.", "10.0" };
for(auto prefix : prefixes){
for(auto& itr : interfaces) {
std::string interfaceIp(itr.second.addr);
if (interfaceIp.find(prefix) == 0 && !(ipv6 && itr.second.addr6.empty())) {
bestPublicIp = itr.second;
break;
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);
}
}
}
return ipv6 ? bestPublicIp.addr6 : bestPublicIp.addr;
}
updatePublicIpAddress(bestPublicIps, {"127.0.0.1", "::1"});
return bestPublicIps;
}
std::string getBestPublicIp()
{
return getBestPublicIp(false);
return getBestPublicIps().addr;
}
} // namespace kiwix

View File

@@ -32,7 +32,7 @@
#endif
#include "tools/stringTools.h"
#include "server/i18n.h"
#include "server/i18n_utils.h"
#include "libkiwix-resources.h"
#include <map>
@@ -384,9 +384,13 @@ void kiwix::Suggestions::add(const zim::SuggestionItem& suggestion)
: suggestion.getTitle();
result.set("label", escapeForJSON(label, DONT_ESCAPE_QUOTE));
result.set("value", escapeForJSON(suggestion.getTitle(), DONT_ESCAPE_QUOTE));
result.set("kind", "path");
result.set("path", escapeForJSON(suggestion.getPath(), DONT_ESCAPE_QUOTE));
if ( suggestion.getPath().empty() ) {
result.set("kind", "modifiedquery");
} else {
result.set("kind", "path");
result.set("value", escapeForJSON(suggestion.getTitle(), DONT_ESCAPE_QUOTE));
result.set("path", escapeForJSON(suggestion.getPath(), DONT_ESCAPE_QUOTE));
}
result.set("first", m_data.is_empty_list());
m_data.push_back(result);
}

View File

@@ -320,16 +320,6 @@ 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
@@ -438,52 +428,6 @@ 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,7 +29,6 @@ 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,6 +32,7 @@
#include <iostream>
#include <iomanip>
#include <regex>
/* tell ICU where to find its dat file (tables) */
void kiwix::loadICUExternalTables()
@@ -256,7 +257,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 (it > value.end() - 3) {
if (value.end() - it < 3) {
os << *it;
continue;
}
@@ -439,3 +440,13 @@ 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

@@ -66,6 +66,9 @@ 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

@@ -15,6 +15,7 @@ skin/i18n/he.json
skin/i18n/hi.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
@@ -23,11 +24,14 @@ 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

View File

@@ -1,18 +1,23 @@
skin/caret.png
skin/bittorrent.png
skin/magnet.png
skin/404.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/kiwix.css
skin/taskbar.css
skin/index.css
skin/fonts/DMSans-Regular.ttf
skin/fonts/Poppins.ttf
skin/fonts/Roboto.ttf
skin/search_results.css
@@ -37,10 +42,11 @@ 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
templates/url_of_search_results_css.tmpl
templates/viewer_settings.js
templates/no_js_library_page.html
templates/no_js_download.html
templates/sexy404.html
opensearchdescription.xml
ft_opensearchdescription.xml
catalog_v2_searchdescription.xml

1
static/skin/404.svg Normal file
View File

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

@@ -23,6 +23,9 @@
.autoComplete_wrapper > ul {
position: absolute;
min-width: 100%;
width: fit-content;
max-width: 600px;
max-height: 226px;
overflow-y: scroll;
top: 100%;
@@ -60,6 +63,11 @@
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

@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -0,0 +1,6 @@
<!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>

After

Width:  |  Height:  |  Size: 546 B

159
static/skin/error.css Normal file
View File

@@ -0,0 +1,159 @@
@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,7 +23,8 @@ const Translations = {
return;
const errorMsg = `Error loading translations for language '${lang}': `;
this.promises[lang] = fetch(`./skin/i18n/${lang}.json`).then(async (resp) => {
const translationJsonUrl = import.meta.resolve(`./i18n/${lang}.json`);
this.promises[lang] = fetch(translationJsonUrl).then(async (resp) => {
if ( resp.ok ) {
this.data[lang] = JSON.parse(await resp.text());
} else {
@@ -190,8 +191,40 @@ function initUILanguageSelector(activeLanguage, languageChangeCallback) {
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,6 +2,7 @@
"@metadata": {
"authors": [
"Asma",
"Hamoudak",
"Ravan",
"محمد أحمد عبد الفتاح"
]
@@ -12,7 +13,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": "لم يتم تقديم ملخص.",
@@ -21,13 +22,31 @@
"400-page-heading": "طلب غير صالح",
"404-page-title": "المحتوى غير موجود",
"404-page-heading": "لم يتم العثور عليه",
"500-page-title": "خطأ في الخادم الداخلي",
"500-page-heading": "خطأ في الخادم الداخلي",
"fulltext-search-unavailable": "البحث عن النص الكامل غير متاح",
"no-search-results": "محرك البحث عن النص الكامل غير متاح لهذا المحتوى.",
"library-button-text": "اذهب لصفحة الترحيب",
"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": "قد يشارك في البحث كتابان أو أكثر بلغات مختلفة، مما قد يؤدي إلى نتائج محيرة."
"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": "أخرى {كتب أخري}"
}

View File

@@ -16,5 +16,15 @@
"search": "অনুসন্ধান",
"welcome-to-kiwix-server": "কিউইক্স সার্ভারে স্বাগতম",
"download-links-title": "বই ডাউনলোড করুন",
"preview-book": "প্রাকদর্শন"
"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,6 +1,7 @@
{
"@metadata": {
"authors": [
"AnnaTheProgrammer777",
"IMayBeABitShy",
"Justman10000",
"Lucas Werkmeister",
@@ -56,12 +57,22 @@
"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",
"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"
"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"
}

View File

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

View File

@@ -20,9 +20,24 @@
, "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" : "Internal Server Error"
, "500-page-text": "An internal server error occured. We are sorry about that :/"
, "external-link-detected" : "External Link Detected"
, "caution-warning" : "Caution!"
, "external-link-intro" : "You are about to leave Kiwix's ZIM reader to go online to"
, "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."
, "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}}"
@@ -51,8 +66,8 @@
, "torrent-download-link-text": "BitTorrent"
, "torrent-download-alt-text": "Download via BitTorrent"
, "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>"
@@ -60,4 +75,23 @@
, "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

@@ -4,6 +4,7 @@
"AlexanderFF",
"Fitoschido",
"Ovruni",
"Sinopsistrans",
"SpikeShroom",
"Vis4valentine"
]
@@ -17,6 +18,7 @@
"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}}",
@@ -26,8 +28,14 @@
"404-page-heading": "No encontrado",
"500-page-title": "Error interno del servidor",
"500-page-heading": "Error interno del servidor",
"500-page-text": "Se produjo un error interno del servidor. Lo sentimos :/",
"fulltext-search-unavailable": "Búsqueda de texto completo no disponible",
"no-search-results": "El motor de búsqueda de texto completo no está disponible para este contenido.",
"search-results-page-title": "Buscar: {{SEARCH_PATTERN}}",
"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",
@@ -41,19 +49,30 @@
"count-of-matching-books": "{{COUNT}} libro(s)",
"download": "Descargar",
"direct-download-link-text": "Directamente",
"direct-download-alt-text": "descarga directa",
"hash-download-link-text": "hash sha256",
"hash-download-alt-text": "descargar hash",
"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 link magnético",
"magnet-alt-text": "Descargar mediante enlace Magnet",
"torrent-download-link-text": "BitTorrent",
"torrent-download-alt-text": "Descargar a través de BitTorrent",
"filter-by-tag": "Filtrar por etiqueta \"{{TAG}}\"",
"stop-filtering-by-tag": "Dejar de filtrar por etiqueta \"{{TAG}}\"",
"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"
"unknown-error": "Error desconocido",
"book-category.wikibooks": "Wikilibros",
"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"
}

View File

@@ -2,7 +2,9 @@
"@metadata": {
"authors": [
"Adriendelucca",
"Benoit74",
"Gomoko",
"Goombiis",
"Melimeli",
"Stephane",
"Thibaut120094",
@@ -60,12 +62,23 @@
"torrent-download-link-text": "BitTorrent",
"torrent-download-alt-text": "Télécharger via BitTorrent",
"library-opds-feed-all-entries": "Flux OPDS de la bibliothèque Toutes les entrées",
"filter-by-tag": "Filtrer par la balise « {{TAG}} »",
"stop-filtering-by-tag": "Arrêter le filtrage par la balise « {{TAG}} »",
"filter-by-tag": "Filtrer par le tag \"{{{TAG}}}\"",
"stop-filtering-by-tag": "Arrêter de filtrer par le tag \"{{{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"
"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"
}

View File

@@ -53,12 +53,24 @@
"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": "שגיאה בלתי־ידועה"
"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": "אחר"
}

View File

@@ -52,5 +52,15 @@
"welcome-to-kiwix-server": "कीविक्स सर्वर में आपका स्वागत है",
"download-links-heading": "<b><i>{{BOOK_TITLE}}</i></b> के लिए डाउनलोड लिंक",
"download-links-title": "पुस्तक डाउनलोड करें",
"preview-book": "पूर्वावलोकन"
"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

@@ -17,5 +17,15 @@
"home-button-text": "Դեպի '{{BOOK_TITLE}}'֊ի գլխավոր էջը",
"random-page-button-text": "Բացել պատահական էջ",
"searchbox-tooltip": "Որոնել '{{BOOK_TITLE}}'֊ում",
"book-filtering-all-categories": "Բոլոր կատեգորիաներ"
"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": "Այլ"
}

View File

@@ -13,7 +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 request valide pro contento crude.",
"invalid-raw-data-type": "{{DATATYPE}} non es un requesta valide pro contento brute.",
"invalid-request": "Le URL requestate “{{{url}}}” non es un requesta valide.",
"no-value-for-arg": "Necun valor fornite pro le argumento {{ARGUMENT}}",
"no-query": "Necun consulta fornite.",
@@ -53,12 +53,22 @@
"torrent-download-link-text": "BitTorrent",
"torrent-download-alt-text": "Discargar per medio de BitTorrent",
"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"
"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"
}

74
static/skin/i18n/id.json Normal file
View File

@@ -0,0 +1,74 @@
{
"@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

@@ -3,6 +3,7 @@
"authors": [
"Albano",
"Beta16",
"Clorofolle",
"Luca.favorido",
"McDutchie"
]
@@ -21,6 +22,9 @@
"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": "Errore interno del server",
"500-page-text": "Si è verificato un errore interno del server. Ci dispiace :/",
@@ -33,15 +37,31 @@
"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",
"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"
"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"
}

View File

@@ -7,15 +7,64 @@
"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": "찾을 수 없음",
"500-page-title": "내부 서버 오류",
"500-page-heading": "내부 서버 오류",
"500-page-text": "내부 서버 오류가 발생했습니다. 죄송합니다 :/",
"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}}개",
"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": "비트토렌트",
"preview-book": "미리 보기"
"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": "기타"
}

View File

@@ -15,9 +15,13 @@
"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": "Interne Feeler um Server",
"500-page-text": "Et ass en interne Serverfeeler opgetrueden. Mir entschëllegen eis dofir :/",
"caution-warning": "Opgepasst!",
"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>",
@@ -36,5 +40,16 @@
"direct-download-link-text": "Direkt",
"torrent-download-link-text": "BitTorrent",
"download-links-title": "Buch eroflueden",
"unknown-error": "Onbekannte Feeler"
"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"
}

View File

@@ -53,12 +53,31 @@
"torrent-download-link-text": "BitTorrent",
"torrent-download-alt-text": "Преземи преку BitTorrent",
"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": "Непозната грешка"
"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": "Ја вчитувам содржината"
}

74
static/skin/i18n/nb.json Normal file
View File

@@ -0,0 +1,74 @@
{
"@metadata": {
"authors": [
"TorgeirS"
]
},
"name": "Engelsk",
"suggest-full-text-search": "inneholder '{{{SEARCH_TERMS}}}'...",
"no-such-book": "Finner ingen slik bok: {{BOOK_NAME}}",
"too-many-books": "Det er forespurt for mange bøker {{NB_BOOKS}} der grensen er {{LIMIT}}",
"no-book-found": "Ingen bøker med treff på utvalgkriteriene",
"url-not-found": "Den forespurte webadressen «{{url}}» ble ikke funnet på denne serveren.",
"suggest-search": "Gjør et fulltekstsøk etter <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
"random-article-failure": "Oops! Klarte ikke å hente en tilfeldig artikkel :(",
"invalid-raw-data-type": "{{DATATYPE}} er ikke en gyldig forespørsel etter uformatert innhold.",
"invalid-request": "Den forespurte URL-en \"{{{url}}}\" er ikke en gyldig forespørsel.",
"no-value-for-arg": "Ingen verdi angitt for argumentet {{ARGUMENT}}",
"no-query": "Ingen søkeord oppgitt.",
"raw-entry-not-found": "Finner ikke {{DATATYPE}}-oppføringen {{ENTRY}}",
"400-page-title": "Ugyldig forespørsel",
"400-page-heading": "Ugyldig forespørsel",
"404-page-title": "Finner ikke innhold",
"404-page-heading": "Ikke funnet",
"500-page-title": "Intern serverfeil",
"500-page-heading": "Intern serverfeil",
"500-page-text": "Det oppstod en intern serverfeil. Vi beklager det :/",
"fulltext-search-unavailable": "Fulltekstsøk utilgjengelig",
"no-search-results": "Søkemotoren for fulltekstsøk er ikke tilgjengelig for dette innholdet.",
"search-results-page-title": "Søk: {{SEARCH_PATTERN}}",
"search-results-page-header": "Resultater <b>{{START}}-{{END}}</b> av <b>{{COUNT}}</b> for <b>\"{{{SEARCH_PATTERN}}}\"</b>",
"empty-search-results-page-header": "Ingen resultater ble funnet for <b>\"{{{SEARCH_PATTERN}}}\"</b>",
"search-result-book-info": "fra {{BOOK_TITLE}}",
"word-count": "{{COUNT}} ord",
"library-button-text": "Gå til velkomstsiden",
"home-button-text": "Gå til hovedsiden for '{{BOOK_TITLE}}'",
"random-page-button-text": "Gå til en tilfeldig valgt side",
"searchbox-tooltip": "Søk etter '{{BOOK_TITLE}}'",
"confusion-of-tongues": "To eller flere bøker på forskjellige språk vil inngå i søket, noe som kan gi forvirrende resultater.",
"welcome-page-overzealous-filter": "Ingen resultater. Vil du <a href=\"{{URL}}\">tilbakestille filteret</a>?",
"powered-by-kiwix-html": "Drevet av <a href=\"https://kiwix.org\">Kiwix</a>",
"search": "Søk",
"book-filtering-all-categories": "Alle kategorier",
"book-filtering-all-languages": "Alle språk",
"count-of-matching-books": "{{COUNT}} bok/bøker",
"download": "Last ned",
"direct-download-link-text": "Direkte",
"direct-download-alt-text": "Last ned direkte via HTTP(S)",
"hash-download-link-text": "SHA-256 kontrollsum",
"hash-download-alt-text": "Vis filens SHA-256 kontrollsum",
"magnet-link-text": "Magnetlenke",
"magnet-alt-text": "Last ned via Magnetlenke",
"torrent-download-link-text": "BitTorrent",
"torrent-download-alt-text": "Last ned via BitTorrent",
"library-opds-feed-all-entries": "Biblioteks OPDS-feed - Alle oppføringer",
"filter-by-tag": "Filtrer etter taggen \"{{{TAG}}}\"",
"stop-filtering-by-tag": "Slutt å filtrere etter taggen \"{{{TAG}}}\"",
"library-opds-feed-parameterised": "Biblioteks OPDS-feed - oppføringer som samsvarer med {{#LANG}}\nSpråk: {{LANG}} {{/LANG}}{{#CATEGORY}}\nKategori: {{CATEGORY}} {{/CATEGORY}}{{#TAG}}\nTagg: {{TAG}} {{/TAG}}{{#Q}}\nSpørring: {{Q}} {{/Q}}",
"welcome-to-kiwix-server": "Velkommen til Kiwix Server",
"download-links-heading": "Nedlastingslenker for <b><i>{{BOOK_TITLE}}</i></b>",
"download-links-title": "Last ned bok",
"preview-book": "Forhåndsvisning",
"unknown-error": "Ukjent feil",
"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": "Annet",
"text-loading-content": "Laster innhold"
}

View File

@@ -1,8 +1,10 @@
{
"@metadata": {
"authors": [
"ABPMAB",
"Kelson",
"McDutchie",
"Siebrand",
"Vistaus"
]
},
@@ -15,6 +17,7 @@
"suggest-search": "In volledige tekst zoeen naar <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
"random-article-failure": "Oeps! Kan geen willekeurig artikel kiezen :(",
"invalid-raw-data-type": "{{DATATYPE}} is geen geldig verzoek voor onbewerkte inhoud.",
"invalid-request": "De gevraagde URL “{{{url}}}” is onjuist.",
"no-value-for-arg": "Er is geen waarde opgegeven bij {{ARGUMENT}}",
"no-query": "Er is geen zoekterm opgegeven.",
"raw-entry-not-found": "Kan het {{DATATYPE}}-item {{ENTRY}} niet vinden",
@@ -24,8 +27,14 @@
"404-page-heading": "Niet gevonden",
"500-page-title": "Interne serverfout",
"500-page-heading": "Interne serverfout",
"500-page-text": "Er is een interne serverfout opgetreden. Dat spijt ons",
"fulltext-search-unavailable": "Zoeken in volledige tekst is niet beschikbaar",
"no-search-results": "De zoekmachine voor volledige tekst is niet beschikbaar voor deze inhoud.",
"search-results-page-title": "Zoeken: {{SEARCH_PATTERN}}",
"search-results-page-header": "Resultaten <b>{{START}}-{{END}}</b> van <b>{{COUNT}}</b> voor <b>“{{{SEARCH_PATTERN}}}”</b>",
"empty-search-results-page-header": "Er zijn geen resultaten gevonden voor <b>“{{{SEARCH_PATTERN}}}”</b>",
"search-result-book-info": "uit {{BOOK_TITLE}}",
"word-count": "{{COUNT}} woorden",
"library-button-text": "Naar de welkomstpagina",
"home-button-text": "Naar de hoofdpagina van {{BOOK_TITLE}}",
"random-page-button-text": "Naar een willekeurig geselecteerde pagina gaan",
@@ -47,11 +56,23 @@
"torrent-download-link-text": "BitTorrent",
"torrent-download-alt-text": "Downloaden via BitTorrent",
"library-opds-feed-all-entries": "OPDS-feed bibliotheek: alle vermeldingen",
"filter-by-tag": "Filteren op label “{{TAG}}”",
"stop-filtering-by-tag": "Niet meer filteren op label “{{TAG}}”",
"filter-by-tag": "Filteren op label “{{{TAG}}}”",
"stop-filtering-by-tag": "Niet meer filteren op label “{{{TAG}}}”",
"library-opds-feed-parameterised": "OPDS-feed bibliotheek: vermeldingen die overeenkomen met {{#LANG}}\nTaal: {{LANG}} {{/LANG}}{{#CATEGORY}}\nCategorie: {{CATEGORY}} {{/CATEGORY}}{{#TAG}}\nLabel: {{TAG}} {{/TAG}}{{#Q}}\nZoekopdracht: {{Q}} {{/Q}}",
"welcome-to-kiwix-server": "Welkom bij de Kiwix-server",
"download-links-heading": "Downloadkoppelingen voor <b><i>{{BOOK_TITLE}}</i></b>",
"download-links-title": "Boek downloaden",
"preview-book": "Voorvertoning"
"preview-book": "Voorvertoning",
"unknown-error": "Onbekende fout",
"book-category.wikibooks": "Wikibooks",
"book-category.wikinews": "Wikinieuws",
"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": "WikiWoordenboek",
"book-category.other": "Overige",
"text-loading-content": "Inhoud laden"
}

View File

@@ -1,7 +1,6 @@
{
"@metadata": {
"authors": [
"Amire80",
"Lancine.kounfantoh.fofana"
]
},

View File

@@ -1,7 +1,8 @@
{
"@metadata": {
"authors": [
"Gouri"
"Gouri",
"Psubhashish"
]
},
"name": "ଓଡ଼ିଆ",
@@ -10,7 +11,7 @@
"too-many-books": "ଅତ୍ୟଧିକ ବହି ଅନୁରୋଧ (${{NB_BOOKS}}) ଯେଉଁଠାରେ ସୀମା ${{LIMIT}} |",
"no-book-found": "କୌଣସି ପୁସ୍ତକ ଚଯ଼ନ ମାନଦଣ୍ଡ ସହ ମେଳ ଖାଉନାହିଁ ।",
"url-not-found": "ଅନୁରୋଧ କରାଯାଇଥିବା URL \"{{url}}\" ଏହି ସର୍ଭରରେ ମିଳିଲା ନାହିଁ |",
"suggest-search": "<a href=\"${{{SEARCH_URL}}}\">${{PATTERN}} for</a> ପାଇଁ ଏକ ସମ୍ପୂର୍ଣ୍ଣ ପାଠ ସନ୍ଧାନ କର |",
"suggest-search": "<a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a> ପାଇଁ ପୂରା ପାଠ ଖୋଜନ୍ତୁ |",
"random-article-failure": "ଓହୋ! ଏକ ଅନିୟମିତ ପ୍ରବନ୍ଧ ବାଛିବାରେ ବିଫଳ :(",
"invalid-raw-data-type": "{{DATATYPE}} କଞ୍ଚା ବିଷୟବସ୍ତୁ ପାଇଁ ଏକ ବ valid ଧ ଅନୁରୋଧ ନୁହେଁ |",
"no-value-for-arg": "ଯୁକ୍ତି ପାଇଁ କୌଣସି ମୂଲ୍ଯ଼ ପ୍ରଦାନ କରାଯାଇନାହିଁ ${{ARGUMENT}}",

View File

@@ -34,5 +34,6 @@
"torrent-download-link-text": "Plik torrent",
"welcome-to-kiwix-server": "Witamy na serwerze Kiwix",
"download-links-title": "Pobierz książkę",
"preview-book": "Podgląd"
"preview-book": "Podgląd",
"book-category.other": "Inne"
}

View File

@@ -2,12 +2,24 @@
"@metadata": {
"authors": [
"Eduardoaddad",
"Klgor1803",
"Obiru",
"Re demz"
"Re demz",
"YoReaper"
]
},
"name": "Português",
"suggest-full-text-search": "Contendo '{{{SEARCH_TERMS}}}'...",
"no-such-book": "Não existe o livro: {{BOOK_NAME}}",
"too-many-books": "Muitos livros solicitados {{NB_BOOKS}} mas o limite é {{LIMIT}}",
"no-book-found": "Nenhum livro corresponde os critérios de seleção",
"url-not-found": "O URL \"{{url}}\" não foi encontrado neste servidor.",
"suggest-search": "Fazer uma pesquisa de texto completo para <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
"random-article-failure": "Ops! Falha ao escolher um artigo aleatório :(",
"invalid-raw-data-type": "{{DATATYPE}} não é uma solicitação válida para conteúdo bruto.",
"invalid-request": "O URL solicitado \"{{{url}}}\" não é uma solicitação válida.",
"no-value-for-arg": "Nenhum valor para o argumento {{ARGUMENT}}",
"no-query": "Nenhuma consulta fornecida.",
"400-page-title": "Requisição inválida",
"400-page-heading": "Requisição inválida",
"404-page-title": "Conteúdo não encontrado",
@@ -16,6 +28,7 @@
"500-page-heading": "Erro interno do servidor",
"500-page-text": "Aconteceu um erro interno do servidor. Nós pedimos desculpas sobre isso :/",
"fulltext-search-unavailable": "Busca por texto completo está indisponível",
"no-search-results": "O motor de busca de texto completo não está disponível para este conteúdo.",
"search-results-page-title": "Buscar: {{SEARCH_PATTERN}}",
"search-results-page-header": "Resultados <b>{{START}}-{{END}}</b> de <b>{{COUNT}}</b> para <b>\"{{{SEARCH_PATTERN}}}\"</b>",
"empty-search-results-page-header": "Nenhum resultado encontrado para <b>\"{{{SEARCH_PATTERN}}}\"</b>",
@@ -26,18 +39,37 @@
"random-page-button-text": "Ir para uma página aleatória",
"searchbox-tooltip": "Buscar '{{BOOK_TITLE}}'",
"confusion-of-tongues": "Dois ou mais livros em diferentes idiomas podem participar da pesquisa, isso pode proporcionar resultados confusos.",
"welcome-page-overzealous-filter": "Nenhum resultado. Gostaria de <a href=\"{{URL}}\">redefinir o filtro</a> ?",
"powered-by-kiwix-html": "Desenvolvido por <a href=\"https://kiwix.org\">Kiwix</a>",
"search": "Pesquisar",
"book-filtering-all-categories": "Todas as categorias",
"book-filtering-all-languages": "Todos os idiomas",
"count-of-matching-books": "{{COUNT}} livro(s)",
"download": "Baixar",
"hash-download-link-text": "Hash sha256",
"hash-download-alt-text": "baixar hash",
"torrent-download-link-text": "Arquivo torrent",
"torrent-download-alt-text": "baixar torrent",
"direct-download-link-text": "Direto",
"direct-download-alt-text": "Baixar diretamente por HTTP(S)",
"hash-download-link-text": "Verificação SHA-256",
"hash-download-alt-text": "Exibir arquivo de verificação SHA-256",
"magnet-link-text": "Link Magnet",
"magnet-alt-text": "Baixar por link Magnet",
"torrent-download-link-text": "BitTorrent",
"torrent-download-alt-text": "Baixar via BitTorrent",
"library-opds-feed-all-entries": "Feed OPDS da biblioteca - Todas as entradas",
"filter-by-tag": "Filtrar por etiqueta \"{{{TAG}}}\"",
"stop-filtering-by-tag": "Parar de filtrar por etiqueta \"{{{TAG}}}\"",
"library-opds-feed-parameterised": "Biblioteca OPDS Feed - entradas que correspondem a {{#LANG}}\nIdioma: {{LANG}} {{/LANG}}{{#CATEGORY}}\nCategoria: {{CATEGORY}} {{/CATEGORY}}{{#TAG}}\nTag: {{TAG}} {{/TAG}}{{#Q}}\nConsulta: {{Q}} {{/Q}}",
"welcome-to-kiwix-server": "Bem vindo ao servidor Kiwix",
"download-links-heading": "Links para baixar <b><i>{{BOOK_TITLE}}</i></b>",
"download-links-title": "Download do livro",
"preview-book": "Pré-visualizar",
"unknown-error": "Erro desconhecido"
"unknown-error": "Erro desconhecido",
"book-category.wikibooks": "Wikilivros",
"book-category.wikinews": "Wikinotícias",
"book-category.wikipedia": "Wikipédia",
"book-category.wikiquote": "Wikiquote",
"book-category.wikisource": "Wikisource",
"book-category.wikispecies": "Wikispécies",
"book-category.wikiversity": "Wikiversidade",
"book-category.wiktionary": "Wikcionário",
"book-category.other": "Outro"
}

73
static/skin/i18n/pt.json Normal file
View File

@@ -0,0 +1,73 @@
{
"@metadata": {
"authors": [
"B3rnas"
]
},
"name": "português",
"suggest-full-text-search": "contendo '{{{SEARCH_TERMS}}}'...",
"no-such-book": "Não existe o livro: {{BOOK_NAME}}",
"too-many-books": "Demasiadas solicitações de livros ({{NB_BOOKS}}) onde o limite é {{LIMIT}}",
"no-book-found": "Nenhum livro corresponde aos critérios de seleção",
"url-not-found": "A URL solicitada \"{{url}}\" não foi encontrada neste servidor.",
"suggest-search": "Faça uma pesquisa de texto completo para <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
"random-article-failure": "Ups! Erro ao eleger um artigo aleatório :(",
"invalid-raw-data-type": "{{DATATYPE}} não é uma solicitação válida para conteúdo bruto.",
"invalid-request": "A URL solicitada \"{{{url}}}\" não é uma solicitação válida.",
"no-value-for-arg": "Nenhum valor fornecido para o argumento {{ARGUMENT}}",
"no-query": "Nenhuma consulta fornecida.",
"raw-entry-not-found": "Não é possível encontrar a entrada {{DATATYPE}} {{ENTRY}}",
"400-page-title": "Solicitação inválida",
"400-page-heading": "Solicitação inválida",
"404-page-title": "Conteúdo não encontrado",
"404-page-heading": "Não encontrado",
"500-page-title": "Erro interno do servidor",
"500-page-heading": "Erro interno do servidor",
"500-page-text": "Ocorreu um erro interno do servidor. Lamentamos por isso :/",
"fulltext-search-unavailable": "Pesquisa de texto completo indisponível",
"no-search-results": "O motor de busca de texto completo não está disponível para este conteúdo.",
"search-results-page-title": "Pesquisar: {{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": "Nenhum resultado foi encontrado para <b>\"{{{SEARCH_PATTERN}}}\"</b>",
"search-result-book-info": "de {{BOOK_TITLE}}",
"word-count": "{{COUNT}} palavras",
"library-button-text": "Ir para página inicial",
"home-button-text": "Vá para a página principal de '{{BOOK_TITLE}}'",
"random-page-button-text": "Vá para uma página selecionada aleatoriamente",
"searchbox-tooltip": "Procurar '{{BOOK_TITLE}}'",
"confusion-of-tongues": "Dois ou mais livros em idiomas diferentes participariam da pesquisa, o que poderia levar a resultados confusos.",
"welcome-page-overzealous-filter": "Nenhum resultado. Gostaria de <a href=\"{{URL}}\">redefinir o filtro</a> ?",
"powered-by-kiwix-html": "Desenvolvido por <a href=\"https://kiwix.org\">Kiwix</a>",
"search": "Pesquisar",
"book-filtering-all-categories": "Todas as categorias",
"book-filtering-all-languages": "Todos os idiomas",
"count-of-matching-books": "{{COUNT}} livro(s)",
"download": "Transferir",
"direct-download-link-text": "Direto",
"direct-download-alt-text": "Descarregar diretamente através de HTTP (S)",
"hash-download-link-text": "Verificação SHA-256",
"hash-download-alt-text": "Exibir arquivo de verificação SHA-256",
"magnet-link-text": "Link magnético",
"magnet-alt-text": "Descarregar através do link Magnet",
"torrent-download-link-text": "BitTorrent",
"torrent-download-alt-text": "Descarregar através de BitTorrent",
"library-opds-feed-all-entries": "Feed OPDS da biblioteca - Todas as entradas",
"filter-by-tag": "Filtrar por tag \"{{TAG}}\"",
"stop-filtering-by-tag": "Pare de filtrar pela tag \"{{TAG}}\"",
"library-opds-feed-parameterised": "Feed OPDS da biblioteca - entradas que correspondem a {{#LANG}}\nIdioma: {{LANG}} {{/LANG}}{{#CATEGORY}}\nCategoria: {{CATEGORY}} {{/CATEGORY}}{{#TAG}}\nTag: {{TAG}} {{/TAG}}{{#Q}}\nConsulta: {{Q}} {{/Q}}",
"welcome-to-kiwix-server": "Bem-vindo ao Kiwix Server",
"download-links-heading": "Links para download de <b><i>{{BOOK_TITLE}}</i></b>",
"download-links-title": "Descarregar livros",
"preview-book": "Pré-visualização",
"unknown-error": "Erro desconhecido",
"book-category.wikibooks": "Wikilivros",
"book-category.wikinews": "Wikinotícias",
"book-category.wikipedia": "Wikipédia",
"book-category.wikiquote": "Wikiquote",
"book-category.wikisource": "Wikisource",
"book-category.wikispecies": "Wikispecies",
"book-category.wikiversity": "Wikiversidade",
"book-category.wikivoyage": "Wikivoyage",
"book-category.wiktionary": "Wikcionário",
"book-category.other": "Outro"
}

View File

@@ -24,9 +24,24 @@
"400-page-heading": "Heading of the 400 error page",
"404-page-title": "Title of the 404 error page",
"404-page-heading": "Heading of the 404 error page",
"new-404-page-title": "Title of the 404 error page",
"new-404-page-heading": "Heading of the 404 error page",
"404-img-text": "Fallback text for the image on the 404 error page",
"path-was-not-found": "Message telling that the URL path was not found (to be followed by the actual path)",
"404-advice.p1": "1st paragraph of the multiline advice on the 'Page not found' error page (see 404-advice.p1 through 404-advice.p5 for full text)",
"404-advice.p2": "2nd paragraph of the multiline advice on the 'Page not found' error page (see 404-advice.p1 through 404-advice.p5 for full text)",
"404-advice.p3": "3rd paragraph of the multiline advice on the 'Page not found' error page (see 404-advice.p1 through 404-advice.p5 for full text)",
"404-advice.p4": "4th paragraph of the multiline advice on the 'Page not found' error page (see 404-advice.p1 through 404-advice.p5 for full text)",
"404-advice.p5": "5th paragraph of the multiline advice on the 'Page not found' error page (see 404-advice.p1 through 404-advice.p5 for full text)",
"500-page-title": "Title of the 500 error page",
"500-page-heading": "Heading of the 500 error page",
"500-page-text": "Text of the 500 error page",
"external-link-detected": "Title & heading of the external link blocker page",
"caution-warning": "Warning of action that shouldn't be carried out carelessly",
"external-link-intro": "Message introducing the external link (to be followed by the actual link)",
"external-link-advice.p1": "1st paragraph of the multiline advice on the external link blocker page (see external-link-advice.p1 through external-link-advice.p3 for full text)",
"external-link-advice.p2": "2nd paragraph of the multiline advice on the external link blocker page (see external-link-advice.p1 through external-link-advice.p3 for full text)",
"external-link-advice.p3": "3rd paragraph of the multiline advice on the external link blocker page (see external-link-advice.p1 through external-link-advice.p3 for full text)",
"fulltext-search-unavailable": "Title of the error page returned when search is attempted in a book without fulltext search database",
"no-search-results": "Text of the error page returned when search is attempted in a book without fulltext search database",
"search-results-page-title": "Title of the search results page",
@@ -62,5 +77,24 @@
"download-links-title": "Title for no-js download page",
"preview-book": "Tooltip of book-tile leading to the book",
"non-translated-text": "{{ignored}}\nUsed to display text that is generated at runtime and cannot be translated. Nothing to translate about this one.",
"unknown-error": "Unknown error"
"unknown-error": "Unknown error",
"book-category.gutenberg": "Name for the category of books from the Gutenberg project",
"book-category.iFixit": "Name for the category of iFixit books",
"book-category.mooc": "Name for the category of MOOC books",
"book-category.phet": "Name for the category of Phet books",
"book-category.stack_exchange": "Name for the category of books from the Stack Exchange network books",
"book-category.ted": "Name for the category of Ted books",
"book-category.vikidia": "Name for the category of Vikidia books",
"book-category.wikibooks": "Name for the category of Wikibooks books books",
"book-category.wikihow": "Name for the category of wikiHow books",
"book-category.wikinews": "Name for the category of Wikinews books",
"book-category.wikipedia": "Name for the category of Wikipedia books",
"book-category.wikiquote": "Name for the category of Wikiquote books",
"book-category.wikisource": "Name for the category of Wikisource books",
"book-category.wikispecies": "Name for the category of Wikispecies books",
"book-category.wikiversity": "Name for the category of Wikiversity books",
"book-category.wikivoyage": "Name for the category of Wikivoyage books",
"book-category.wiktionary": "Name for the category of Wiktionary books",
"book-category.other": "Books not belonging to any special category are listed under this one",
"text-loading-content": "Text displayed while content is being loaded"
}

75
static/skin/i18n/ro.json Normal file
View File

@@ -0,0 +1,75 @@
{
"@metadata": {
"authors": [
"MSClaudiu",
"Trotinel Iftode"
]
},
"name": "Engleză",
"suggest-full-text-search": "care conține '{{{SEARCH_TERMS}}}'...",
"no-such-book": "Nu există o astfel de carte: {{BOOK_NAME}}",
"too-many-books": "Prea multe cărți solicitate ({{NB_BOOKS}}) unde limita este {{LIMIT}}",
"no-book-found": "Nicio carte nu corespunde criteriilor de selecție",
"url-not-found": "Adresa URL solicitată \"{{url}}\" nu a fost găsită pe acest server.",
"suggest-search": "Efectuați o căutare text complet pentru <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
"random-article-failure": "Hopa! Nu s-a putut alege un articol aleatoriu :(",
"invalid-raw-data-type": "{{DATATYPE}} nu este o solicitare validă pentru conținut brut.",
"invalid-request": "Adresa URL solicitată \"{{{url}}}\" nu este o solicitare validă.",
"no-value-for-arg": "Nu este furnizată nicio valoare pentru argumentul {{ARGUMENT}}",
"no-query": "Nu a fost furnizată nicio interogare.",
"raw-entry-not-found": "Nu se poate găsi {{DATATYPE}} intrarea {{ENTRY}}",
"400-page-title": "Cerere invalidă",
"400-page-heading": "Cerere invalidă",
"404-page-title": "Conținut nu a fost găsit",
"404-page-heading": "Nu a fost găsit",
"500-page-title": "Eroare internă de server",
"500-page-heading": "Eroare internă de server",
"500-page-text": "A apărut o eroare internă de server. Ne pare rau pentru asta :/",
"external-link-intro": "Ești pe cale să părăsești cititorul ZIM al Kiwix pentru a te conecta la...",
"fulltext-search-unavailable": "Căutarea text integral indisponibilă",
"no-search-results": "Motorul de căutare textintegral nu este disponibil pentru acest conținut.",
"search-results-page-title": "Căutare: {{SEARCH_PATTERN}}",
"search-results-page-header": "Rezultatele <b>{{START}}-{{END}}</b> din <b>{{COUNT}}</b> pentru <b>„{{{SEARCH_PATTERN}}}”</b>",
"empty-search-results-page-header": "Nu au fost găsite rezultate pentru <b>„{{{SEARCH_PATTERN}}}”</b>",
"search-result-book-info": "din {{BOOK_TITLE}}",
"word-count": "{{COUNT}} cuvinte",
"library-button-text": "Mergi la pagina de pornire",
"home-button-text": "Accesați pagina principală a „{{BOOK_TITLE}}”",
"random-page-button-text": "Accesați o pagină selectată aleatoriu",
"searchbox-tooltip": "Căutați „{{BOOK_TITLE}}”",
"confusion-of-tongues": "Două sau mai multe cărți în limbi diferite ar participa la căutare, ceea ce poate duce la rezultate confuze.",
"welcome-page-overzealous-filter": "Nici un rezultat. Doriți să <a href=\"{{URL}}\">resetați filtrul</a>?",
"powered-by-kiwix-html": "Susținut de&nbsp;<a href=\"https://kiwix.org\">Kiwix</a>",
"search": "Caută",
"book-filtering-all-categories": "Toate categoriile",
"book-filtering-all-languages": "Toate limbile",
"count-of-matching-books": "{{COUNT}} cărți",
"download": "Descărcare",
"direct-download-link-text": "Direct",
"direct-download-alt-text": "Descărcați direct prin HTTP(S)",
"hash-download-link-text": "Sumă de control SHA-256",
"hash-download-alt-text": "Afișează suma de verificare a fișierului SHA-256",
"magnet-link-text": "Legătură magnetică",
"magnet-alt-text": "Descărcați prin linkul Magnet",
"torrent-download-link-text": "BitTorrent",
"torrent-download-alt-text": "Descărcați prin BitTorrent",
"library-opds-feed-all-entries": "Bibliotecă OPDS Feed - Toate intrările",
"filter-by-tag": "Filtrați după eticheta \"{{{TAG}}}\"",
"stop-filtering-by-tag": "Opriți filtrarea după eticheta \"{{{TAG}}}\"",
"library-opds-feed-parameterised": "Bibliotecă OPDS Feed - intrări care se potrivesc cu {{#LANG}}\nLimba: {{LANG}} {{/LANG}}{{#CATEGORY}}\nCategorie: {{CATEGORY}} {{/CATEGORY}}{{#TAG}}\nEtichetă: {{TAG}} {{/TAG}}{{#Q}}\nInterogare: {{Q}} {{/Q}}",
"welcome-to-kiwix-server": "Bun venit la Kiwix Server",
"download-links-heading": "Descărcați linkuri pentru <b><i>{{BOOK_TITLE}}</i></b>",
"download-links-title": "Descărcă cartea",
"preview-book": "Previzualizare",
"unknown-error": "Eroare necunoscută",
"book-category.wikibooks": "Wikimanuale",
"book-category.wikinews": "Wikiștiri",
"book-category.wikipedia": "Wikipedia",
"book-category.wikiquote": "Wikicitat",
"book-category.wikisource": "Wikisursă",
"book-category.wikispecies": "Wikispecii",
"book-category.wikiversity": "Wikiversitate",
"book-category.wikivoyage": "Wikivoyage",
"book-category.wiktionary": "Wikționar",
"book-category.other": "Altul"
}

View File

@@ -3,6 +3,7 @@
"authors": [
"Fenixs-ru",
"Kareyac",
"Lutece398",
"Okras",
"Pacha Tchernof",
"Razno0",
@@ -58,12 +59,22 @@
"torrent-download-link-text": "BitTorrent",
"torrent-download-alt-text": "Скачать через BitTorrent",
"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}}\nLanguage: {{LANG}} {{/LANG}}{{#CATEGORY}}\nCategory: {{CATEGORY}} {{/CATEGORY}} {{#TAG}}\nTag: {{TAG}} {{/TAG}}{{#Q}}\nЗапрос: {{Q}} {{/Q}}",
"welcome-to-kiwix-server": "Добро пожаловать на сервер Kiwix",
"download-links-heading": "Ссылки для скачивания <b><i>{{BOOK_TITLE}}</i></b>",
"download-links-title": "Скачать книгу",
"preview-book": "Предпросмотр",
"unknown-error": "Неизвестная ошибка"
"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": "Другое"
}

View File

@@ -38,16 +38,16 @@
"count-of-matching-books": "{{COUNT}} libru/os",
"download": "Iscàrriga",
"direct-download-link-text": "Diretu",
"direct-download-alt-text": "iscarrigamentu diretu",
"hash-download-link-text": "Hash SHA256",
"hash-download-alt-text": "hash de s'iscarrigamentu",
"direct-download-alt-text": "Iscàrriga in manera direta tràmite HTTP(S)",
"hash-download-link-text": "Summa de controllu SHA-256",
"hash-download-alt-text": "Mustra sa summa de controllu SHA-256",
"magnet-link-text": "Ligàmene Magnet",
"magnet-alt-text": "ligàmene \"magnet\" de iscarrigamentu",
"torrent-download-link-text": "Documentu Torrent",
"torrent-download-alt-text": "iscàrriga su torrent",
"magnet-alt-text": "Iscàrriga impreende su ligàmene Magnet",
"torrent-download-link-text": "BitTorrent",
"torrent-download-alt-text": "Iscàrriga impreende BitTorrent",
"library-opds-feed-all-entries": "Flussu OPDS de sa biblioteca Totu sos elementos",
"filter-by-tag": "Filtra pro eticheta \"{{TAG}}\"",
"stop-filtering-by-tag": "Non filtres prus pro eticheta \"{{TAG}}\"",
"filter-by-tag": "Filtra pro eticheta \"{{{TAG}}}\"",
"stop-filtering-by-tag": "Non filtres prus pro eticheta \"{{{TAG}}}\"",
"library-opds-feed-parameterised": "Flussu OPDS de sa biblioteca - elementos chi currispondet cun {{#LANG}}\nLimba: {{LANG}} {{/LANG}}{{#CATEGORY}}\nCategoria: {{CATEGORY}} {{/CATEGORY}}{{#TAG}}\nEticheta: {{TAG}} {{/TAG}}{{#Q}}\nChirca: {{Q}} {{/Q}}",
"welcome-to-kiwix-server": "Bene bènnidu a su serbidore de Kiwix",
"download-links-heading": "Ligàmenes de iscarrigamentu pro <b><i>{{BOOK_TITLE}}</i></b>",

View File

@@ -9,6 +9,8 @@
"400-page-heading": "غلط ارداس",
"404-page-title": "مواد کائنی لبھیا",
"404-page-heading": "کائنی لبھا",
"new-404-page-title": "ورقہ کائنی لبھیا",
"404-img-text": "کائنی لبھا!",
"500-page-title": "اندرلا سرور نقص",
"500-page-heading": "اندرلا سرور نقص",
"search": "ڳولو",
@@ -22,5 +24,16 @@
"torrent-download-link-text": "ٹورنٹ فائل",
"torrent-download-alt-text": "ٹورںٹ ݙاؤن لوڈ کرو",
"download-links-title": "کتاب ڈاؤن لوڈ کرو",
"preview-book": "پیشگی ݙکھالا"
"preview-book": "پیشگی ݙکھالا",
"book-category.vikidia": "وکی ڈیا",
"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

@@ -10,7 +10,7 @@
"suggest-full-text-search": "vsebuje »{{{SEARCH_TERMS}}}« ...",
"no-such-book": "Ni take knjige: {{BOOK_NAME}}",
"too-many-books": "Preveč zahtevanih knjig ({{NB_BOOKS}}), omejitev je {{LIMIT}}",
"no-book-found": "Izbirnim merilom ne ustreza nobena knjiga",
"no-book-found": "Izbranim merilom ne ustreza nobena knjiga",
"url-not-found": "Zahtevanega URL-ja »{{url}}« v tem strežniku ni bilo mogoče najti.",
"suggest-search": "Preiščite celotno besedilo za <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
"random-article-failure": "Ups! Ni bilo mogoče izbrati naključnega članka :(",

View File

@@ -13,6 +13,7 @@
"suggest-search": "Bëni një kërkim të plotë teksti për <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
"random-article-failure": "Oh! Su arrit të merrej një artikull kuturu :(",
"invalid-raw-data-type": "{{DATATYPE}} sështë varg i vlefshëm kërkimi për lëndë të papërpunuar.",
"invalid-request": "URL-ja e kërkuar “{{{url}}}” sështë kërkesë e vlefshme.",
"no-value-for-arg": "Su dha vlerë për argumentin {{ARGUMENT}}",
"no-query": "Su dha varg kërkimi.",
"raw-entry-not-found": "Sgjendet dot zëri {{DATATYPE}} {{ENTRY}}",
@@ -22,8 +23,14 @@
"404-page-heading": "Su Gjet",
"500-page-title": "Gabim i Brendshëm Shërbyesi",
"500-page-heading": "Gabim i Brendshëm Shërbyesi",
"500-page-text": "Ndodhi një gabim i brendshëm shërbyesi. Na ndjeni për këtë :/",
"fulltext-search-unavailable": "Kërkim teksti të plotë jo i mundshëm",
"no-search-results": "Sështë i passhëm motori i kërkimit të tekstit të plotë për këtë lëndë.",
"search-results-page-title": "Kërkim: {{SEARCH_PATTERN}}",
"search-results-page-header": "Përfundime <b>{{START}}-{{END}}</b> nga <b>{{COUNT}}</b> gjithsej për <b>\"{{{SEARCH_PATTERN}}}\"</b>",
"empty-search-results-page-header": "Su gjetën përfundime për <b>“{{{SEARCH_PATTERN}}}”</b>",
"search-result-book-info": "nga {{BOOK_TITLE}}",
"word-count": "{{COUNT}} fjalë",
"library-button-text": "Kalo te faqja e mirëseardhjes",
"home-button-text": "Kalo te faqja krye e '{{BOOK_TITLE}}'",
"random-page-button-text": "Kalo te një faqe e përzgjedhur kuturu",
@@ -37,19 +44,30 @@
"count-of-matching-books": "{{COUNT}} libër(a)",
"download": "Shkarkoje",
"direct-download-link-text": "Drejtpërsëdrejti",
"direct-download-alt-text": "shkarkim i drejtpërdrejt",
"hash-download-link-text": "Hash sha256",
"hash-download-alt-text": "shkarko hashin",
"direct-download-alt-text": "Shkarkoje drejtpërdrejti përmes HTTP(S)",
"hash-download-link-text": "“Checksum” SHA-256",
"hash-download-alt-text": "Shfaq “checksum” SHA-256 kartele",
"magnet-link-text": "Lidhje Magnet",
"magnet-alt-text": "shkarko magnetin",
"torrent-download-link-text": "Kartelë Torrent",
"torrent-download-alt-text": "shkarko torrent-in",
"magnet-alt-text": "Shkarkoje përmes lidhjeje Magnet",
"torrent-download-link-text": "BitTorrent",
"torrent-download-alt-text": "Shkarkoje përmes BitTorrent-it",
"library-opds-feed-all-entries": "Prurje OPDS Biblioteke - Krejt zërat",
"filter-by-tag": "Filtroji sipas etiketës “{{TAG}}”",
"stop-filtering-by-tag": "Resht së filtruari sipas etiketë “{{TAG}}”",
"filter-by-tag": "Filtroji sipas etikete “{{{TAG}}}”",
"stop-filtering-by-tag": "Resht së filtruari sipas etikete “{{{TAG}}}”",
"library-opds-feed-parameterised": "Prurje OPDS Biblioteke - zëra që kanë përputhje me {{#LANG}}\nGjuhë: {{LANG}} {{/LANG}}{{#CATEGORY}}\nKategori: {{CATEGORY}} {{/CATEGORY}}{{#TAG}}\nEtiketë: {{TAG}} {{/TAG}}{{#Q}}\nVarg Kërkimi: {{Q}} {{/Q}}",
"welcome-to-kiwix-server": "Mirë se vini në Shërbyesin Kiwix",
"download-links-heading": "Lidhje shkarkimi për <b><i>{{BOOK_TITLE}}</i></b>",
"download-links-title": "Shkarkoje librin",
"preview-book": "Paraparje"
"preview-book": "Paraparje",
"unknown-error": "Gabim i panjohur",
"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": "Tjetër"
}

View File

@@ -2,6 +2,7 @@
"@metadata": {
"authors": [
"Jopparn",
"Larsa",
"Rofiatmustapha12",
"Sabelöga",
"WikiPhoenix"
@@ -55,12 +56,22 @@
"torrent-download-link-text": "BitTorrent",
"torrent-download-alt-text": "Ladda ner via BitTorrent",
"library-opds-feed-all-entries": "Library OPDS Feed - Alla poster",
"filter-by-tag": "Filtrera efter taggen \"{{TAG}}\"",
"stop-filtering-by-tag": "Sluta filtrera efter taggen \"{{TAG}}\"",
"filter-by-tag": "Filtrera efter taggen \"{{{TAG}}}\"",
"stop-filtering-by-tag": "Sluta filtrera efter taggen \"{{{TAG}}}\"",
"library-opds-feed-parameterised": "Library OPDS Feed - poster som matchar {{#LANG}}\nSpråk: {{LANG}} {{/LANG}}{{#CATEGORY}}\nKategori: {{CATEGORY}} {{/CATEGORY}}{{#TAG}}\nTagg: {{TAG}} {{/TAG}}{{#Q}}\nFråga: {{Q}} {{/Q}}",
"welcome-to-kiwix-server": "Välkommen till Kiwix Server",
"download-links-heading": "Nedladdningslänkar för <b><i>{{BOOK_TITLE}}</i></b>",
"download-links-title": "Ladda ned bok",
"preview-book": "Förhandsgranska",
"unknown-error": "Okänt fel"
"unknown-error": "Okänt fel",
"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": "Övriga"
}

View File

@@ -1,6 +1,8 @@
{
"@metadata": {
"authors": [
"Joeamj",
"Muddyb",
"Peggy",
"Wangombe"
]
@@ -40,7 +42,7 @@
"welcome-page-overzealous-filter": "Hakuna matokeo. Je, ungependa <a href=\"{{URL}}\">kuweka upya kichujio</a> ?",
"powered-by-kiwix-html": "Inaendeshwa na <a href=\"https://kiwix.org\">Kiwix</a>",
"search": "Tafuta",
"book-filtering-all-categories": "Kategoria Zote",
"book-filtering-all-categories": "Jamii Zote",
"book-filtering-all-languages": "Lugha zote",
"count-of-matching-books": "Vitabu {{COUNT}}",
"download": "Pakua",
@@ -60,5 +62,6 @@
"download-links-heading": "Pakua viungo vya <b><i>{{BOOK_TITLE}}</i></b>",
"download-links-title": "Pakua vitabu",
"preview-book": "Hakiki",
"unknown-error": "Hitilafu isiyojulikana"
"unknown-error": "Hitilafu isiyojulikana",
"text-loading-content": "Inapakia Maudhui"
}

View File

@@ -1,7 +1,6 @@
{
"@metadata": {
"authors": [
"Amire80",
"Chaduvari",
"Rishitha 1238",
"V Bhavya"

View File

@@ -13,6 +13,21 @@
, "400-page-heading": "[I18N TESTING] -400 karma for an invalid request"
, "404-page-title": "[I18N TESTING] Not Found - Try Again"
, "404-page-heading": "[I18N TESTING] Content not found, but at least the server is alive"
, "new-404-page-title" : "Page [I18N] not [TESTING] found"
, "new-404-page-heading" : "[I18N TESTING] Oops. Larry Page could not be reached. He may be on paternity leave."
, "404-img-text": "[I18N] Not found! [TESTING]"
, "path-was-not-found": "[I18N TESTING] The requested path was not found (in fact, nothing was found instead, either):"
, "404-advice.p1": "Sh*t happens. [I18N TESTING] Take it easy!"
, "404-advice.p2": "[I18N TESTING] Try one of the following:"
, "404-advice.p3": "[I18N TESTING] Check the spelling of the URL path"
, "404-advice.p4": "[I18N TESTING] Press the dice button"
, "404-advice.p5": "Good luck! [I18N TESTING]"
, "external-link-detected" : "External [I18] Link [TESTING] Detected"
, "caution-warning" : "[I18N] C5n! [TESTING]"
, "external-link-intro" : "[I18N TESTING] The following link may lead you to a place from which you won't ever be able to return"
, "external-link-advice.p1": "[I18N TESTING] The link you're trying to access points past the end of the ZIM file."
, "external-link-advice.p2": "[I18N TESTING] If you are an optimist, you can attempt to open the link."
, "external-link-advice.p3": "[I18N TESTING] But we strongly recommend you to be reasonable and press your browser's back button."
, "library-button-text": "[I18N TESTING] Navigate to the welcome page"
, "home-button-text": "[I18N TESTING] Jump to the main page of '{{BOOK_TITLE}}'"
, "random-page-button-text": "[I18N TESTING] I am tired of determinism"
@@ -47,4 +62,23 @@
, "empty-search-results-page-header": "[I18N TESTING] No results were found for <b>\"{{{SEARCH_PATTERN}}}\"</b>"
, "search-result-book-info": "from [I18N TESTING] {{BOOK_TITLE}}"
, "word-count": "{{COUNT}} [I18N TESTING] words"
, "book-category.gutenberg": "[I18N] Gutenberg [TESTING]"
, "book-category.iFixit": "[I18N] iFixit [TESTING]"
, "book-category.mooc": "[I18N] MOOC [TESTING]"
, "book-category.phet": "[I18N] Phet [TESTING]"
, "book-category.stack_exchange": "[I18N] Stack Exchange [TESTING]"
, "book-category.ted": "[I18N] Ted [TESTING]"
, "book-category.vikidia": "[I18N] Vikidia [TESTING]"
, "book-category.wikibooks": "[I18N] Wikibooks [TESTING]"
, "book-category.wikihow": "[I18N] wikiHow [TESTING]"
, "book-category.wikinews": "[I18N] Wikinews [TESTING]"
, "book-category.wikipedia": "[I18N] Wikipedia [TESTING]"
, "book-category.wikiquote": "[I18N] Wikiquote [TESTING]"
, "book-category.wikisource": "[I18N] Wikisource [TESTING]"
, "book-category.wikispecies": "[I18N] Wikispecies [TESTING]"
, "book-category.wikiversity": "[I18N] Wikiversity [TESTING]"
, "book-category.wikivoyage": "[I18N] Wikivoyage [TESTING]"
, "book-category.wiktionary": "[I18N] Wiktionary [TESTING]"
, "book-category.other": "[I18N] Other [TESTING]"
, "text-loading-content": "[I18N TESTING] Loading content"
}

View File

@@ -4,6 +4,7 @@
"GuoPC",
"IceButBin",
"Kichin",
"Prmsh",
"StarrySky",
"Sunai",
"XtexChooser",
@@ -17,7 +18,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}} 对原请求无效。",
"invalid-request": "请求的URL无效{{{url}}}",
"no-value-for-arg": "参数{{ARGUMENT}}无值",
@@ -58,12 +59,23 @@
"torrent-download-link-text": "BitTorrent",
"torrent-download-alt-text": "通过 BitTorrent 下载",
"library-opds-feed-all-entries": "图书馆 OPDS Feed - 所有条目",
"filter-by-tag": "按标签“{{TAG}}”过滤",
"stop-filtering-by-tag": "停止按标签“{{TAG}}”过滤",
"filter-by-tag": "按标签“{{{TAG}}}”过滤",
"stop-filtering-by-tag": "停止按标签“{{{TAG}}}”过滤",
"library-opds-feed-parameterised": "图书馆 OPDS Feed - 匹配的项目 {{#LANG}}\n语言{{LANG}} {{/LANG}}{{#CATEGORY}}\n分类{{CATEGORY}} {{/CATEGORY}}{{#TAG}}\n标签{{TAG}} {{/TAG}}{{#Q}}\n查询{{Q}} {{/Q}}",
"welcome-to-kiwix-server": "欢迎来到 Kiwix 服务器",
"download-links-heading": "下载<b><i>{{BOOK_TITLE}}</i></b>的链接",
"download-links-title": "下载书籍",
"preview-book": "预览",
"unknown-error": "未知错误"
"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": "其他",
"text-loading-content": "内容加载中"
}

View File

@@ -46,20 +46,39 @@
"count-of-matching-books": "{{COUNT}} 本書籍",
"download": "下載",
"direct-download-link-text": "直接",
"direct-download-alt-text": "直接下載",
"hash-download-link-text": "Sha256 雜湊",
"hash-download-alt-text": "下載雜湊",
"direct-download-alt-text": "直接透過 HTTPS下載",
"hash-download-link-text": "SHA-256 核對和",
"hash-download-alt-text": "顯示 SHA-256 檔案核對和",
"magnet-link-text": "Magnet 連結",
"magnet-alt-text": "下載 magnet",
"torrent-download-link-text": "Torrent 檔案",
"torrent-download-alt-text": "下載 torrent",
"magnet-alt-text": "透過磁力連結下載",
"torrent-download-link-text": "BitTorrent",
"torrent-download-alt-text": "透過 BitTorrent 下載",
"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": "歡迎來到 Kiwix 伺服器",
"download-links-heading": "下載<b><i>{{BOOK_TITLE}}</i></b>的連結",
"download-links-title": "下載書籍",
"preview-book": "預覽",
"unknown-error": "不明錯誤"
"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": "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": "正在載入內容"
}

View File

@@ -121,7 +121,7 @@
.tagFilterLabel {
width: max-content;
padding: 10px;
padding: 7px;
font-family: roboto;
font-size: 12px;
margin: 0 0 0 17px;
@@ -152,23 +152,24 @@
.book__link {
text-decoration: none;
grid-column: 1 / 3;
grid-row: 1 / 3;
}
.book__wrapper {
--tile-width: 250px;
--tile-border-width: 1px;
--tile-border-radius: 3px;
color: #444343;
height: 248px;
width: 250px;
height: 258px;
width: var(--tile-width);
margin: 15px;
background-color: #f7f7f7;
border: 1px solid #ececec;
border-radius: 3px;
display: grid;
grid-template-columns: 65px 1fr;
grid-template-rows: 70px 120px 1fr 1fr;
grid-gap: 5px;
border: var(--tile-border-width) solid #ececec;
border-radius: var(--tile-border-radius);
display: flex;
flex-direction: column;
justify-content: space-between;
transition: transform 0.25s;
gap: 4px;
}
.book__wrapper:hover {
@@ -177,8 +178,12 @@
.book__link__wrapper {
display: grid;
grid-template-areas:
"bookIcon bookHeader"
"bookDesc bookDesc"
;
grid-template-columns: 65px 1fr;
grid-template-rows: 70px 120px 1fr 1fr;
grid-template-rows: 70px 120px;
}
.book__icon {
@@ -189,7 +194,10 @@
align-items: center;
align-content: center;
justify-content: center;
width: 48px;
height: 48px;
margin: 10px 0 0 10px;
grid-area: bookIcon;
}
.book__header {
@@ -201,6 +209,7 @@
height: 100%;
align-items: center;
align-content: center;
grid-area: bookHeader;
}
.book__title {
@@ -211,28 +220,36 @@
}
.book__download {
font-size: 1.1rem;
margin: 3px 0;
cursor: pointer;
font-size: 1.2rem;
background: #00b4e4;
padding: 8px;
border-radius: 0 0 var(--tile-border-radius) var(--tile-border-radius);
display: flex;
align-items: center;
justify-content: center;
position: relative;
width: var(--tile-width);
left: calc(0px - var(--tile-border-width));
text-decoration: none;
}
.book__download > img {
height: 16px;
width: 16px;
}
.book__download > span {
cursor: pointer;
text-decoration: none;
position: relative;
padding: 0 3px 1px;
font-family: roboto;
background: #00b4e4;
color: white;
box-shadow: 0 0 0 0;
border-radius: 2px;
}
.book__download > span:hover {
box-shadow: 0 5px 5px rgba(0, 0, 0, 0.1)
.book__download:hover {
background: royalblue;
}
.book__description {
grid-column: 1 / 3;
margin: 10px 10px 5px;
overflow: hidden;
display: -webkit-box;
@@ -243,6 +260,13 @@
font-size: 1.2rem;
color: #4d4d4d;
line-height: 1.25;
grid-area: bookDesc;
}
.book__meta {
display: flex;
justify-content: space-between;
padding: 0 10px 4px;
}
.book__languageTag {
@@ -255,7 +279,6 @@
color: black;
height: 25px;
width: 25px;
margin: 10px auto 0 10px;
border-radius: 5px;
font-size: 0.85rem;
}
@@ -266,8 +289,6 @@
font-size: 1.1rem;
justify-content: flex-end;
font-family: roboto;
margin-right: 10px;
margin-top: 10px;
overflow: hidden;
}

View File

@@ -105,6 +105,14 @@
return '';
}
// Borrowed from https://stackoverflow.com/a/1912522
function htmlDecode(input){
var e = document.createElement('textarea');
e.innerHTML = input;
// handle case of empty input
return e.childNodes.length === 0 ? "" : e.childNodes[0].nodeValue;
}
function htmlEncode(str) {
return str.replace(/[\u00A0-\u9999<>\&]/gim, (i) => `&#${i.charCodeAt(0)};`);
}
@@ -121,9 +129,27 @@
function generateTagLink(tagValue) {
tagValue = tagValue.toLowerCase();
const humanFriendlyTagValue = humanFriendlyTitle(tagValue);
const tagMessage = $t("filter-by-tag", {TAG: humanFriendlyTagValue});
return `<span class='tag__link' aria-label='${tagMessage}' title='${tagMessage}' data-tag=${tagValue}>${humanFriendlyTagValue}</span>`
const tagMessage = $t("filter-by-tag", {TAG: tagValue});
const spanElement = document.createElement("span");
spanElement.className = 'tag__link';
spanElement.setAttribute('aria-label', tagMessage);
spanElement.setAttribute('title', tagMessage);
spanElement.setAttribute('data-tag', tagValue);
spanElement.innerHTML = htmlEncode(tagValue);
return spanElement.outerHTML;
}
function downloadButtonText(zimSize) {
return $t("download") + (zimSize ? ` - ${zimSize}`: '');
}
function downloadButtonHtml(url, zimSize) {
return url
? `<div class="book__download" data-link="${url}" data-size="${zimSize}">
<img src="${root}/skin/download-white.svg?KIWIXCACHEID">
<span ">${downloadButtonText(zimSize)}</span>
</div>`
: '';
}
function generateBookHtml(book, sort = false) {
@@ -144,7 +170,7 @@
const mulLangList = langCodesList.filter(x => languages.hasOwnProperty(x)).map(x => languages[x]);
language = mulLangList.join(', ');
}
const tags = getInnerHtml(book, 'tags');
const tags = htmlDecode(getInnerHtml(book, 'tags'));
const tagList = tags.split(';').filter(tag => {return !(tag.startsWith('_'))});
const tagFilterLinks = tagList.map((tagValue) => generateTagLink(tagValue));
const tagHtml = tagFilterLinks.join(' | ');
@@ -170,6 +196,7 @@
}
const faviconAttr = iconUrl != undefined ? `style="background-image: url('${iconUrl}')"` : '';
const languageAttr = langCode != '' ? `title="${language}" aria-label="${language}"` : 'style="background-color: transparent"';
divTag.innerHTML = `
<div class="book__wrapper">
<a class="book__link" href="${viewerLink}" data-hover="Preview">
@@ -177,13 +204,15 @@
<div class="book__icon" ${faviconAttr}></div>
<div class="book__header">
<div id="book__title">${title}</div>
${downloadLink ? `<div class="book__download"><span data-link="${downloadLink}">${$t("download")} ${humanFriendlyZimSize ? ` - ${humanFriendlyZimSize}</span></div>`: ''}` : ''}
</div>
<div class="book__description" title="${description}">${description}</div>
</div>
</a>
<div class="book__meta">
<div class="book__languageTag" ${languageAttr}>${getLanguageCodeToDisplay(langCode)}</div>
<div class="book__tags"><div class="book__tags--wrapper">${tagHtml}</div></div>
</div>
${downloadButtonHtml(downloadLink, humanFriendlyZimSize)}
</div></div>`;
return divTag;
}
@@ -209,7 +238,8 @@
let finalValue = (keysToURIEncode.indexOf(key) >= 0) ? encodeURIComponent(value) : value;
output += `&${key}=${finalValue}`;
}
return output;
// exclude first char so the first params are not prefixed with &
return output.substring(1);
}
/* hack for library.kiwix.org magnet links (created by MirrorBrain)
@@ -269,6 +299,7 @@
}
function insertModal(button) {
const downloadSize = button.getAttribute('data-size');
const downloadLink = button.getAttribute('data-link');
button.addEventListener('click', async (event) => {
event.preventDefault();
@@ -278,7 +309,7 @@
<div class="modal-heading">
<div class="modal-title">
<div>
Download
${downloadButtonText(downloadSize)}
</div>
</div>
<div onclick="closeModal()" class="modal-close-button">
@@ -438,7 +469,7 @@
booksToDelete.forEach(book => {iso.remove(book);});
books.forEach((book) => {
iso.insert(generateBookHtml(book, sort))
const downloadButton = document.querySelector(`[data-id="${getInnerHtml(book, 'id')}"] .book__download span`);
const downloadButton = document.querySelector(`[data-id="${getInnerHtml(book, 'id')}"] .book__download`);
if (downloadButton) {
insertModal(downloadButton);
}
@@ -492,7 +523,7 @@
function addTagElement(tagValue, resetFilter) {
const tagElement = document.getElementsByClassName('tagFilterLabel')[0];
tagElement.style.display = 'inline-block';
tagElement.innerHTML = `${tagValue}`;
tagElement.innerHTML = htmlEncode(tagValue);
const tagMessage = $t("stop-filtering-by-tag", {TAG: tagValue});
tagElement.setAttribute('aria-label', tagMessage);
tagElement.setAttribute('title', tagMessage);

View File

@@ -15,6 +15,35 @@ body {
box-sizing: border-box;
}
.loader {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 24px;
background: rgba(255, 255, 255, 0.8);
padding: 10px 20px;
border-radius: 5px;
display: flex;
align-items: center;
gap: 10px;
white-space: nowrap; /* Prevents text from wrapping */
min-width: fit-content; /* Ensures the container doesnt shrink */
}
.spinner {
width: 20px;
height: 20px;
border: 3px solid #ccc;
border-top: 3px solid #007bff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.hidden {
visibility: hidden;
}
::selection {
background-color: #00b4e4;
color: white;
@@ -96,6 +125,11 @@ body {
height: 30px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@font-face {
font-family: "poppins";
src: url("../skin/fonts/Poppins.ttf?KIWIXCACHEID") format("truetype");

View File

@@ -2,12 +2,12 @@ const uiLanguages = [
{
"iso_code": "ar",
"self_name": "الإنجليزية",
"translation_count": 25
"translation_count": 43
},
{
"iso_code": "bn",
"self_name": "বাংলা",
"translation_count": 14
"translation_count": 24
},
{
"iso_code": "br",
@@ -27,7 +27,7 @@ const uiLanguages = [
{
"iso_code": "de",
"self_name": "Deutsch",
"translation_count": 57
"translation_count": 67
},
{
"iso_code": "el",
@@ -37,12 +37,12 @@ const uiLanguages = [
{
"iso_code": "en",
"self_name": "English",
"translation_count": 58
"translation_count": 77
},
{
"iso_code": "es",
"self_name": "español",
"translation_count": 49
"translation_count": 67
},
{
"iso_code": "fi",
@@ -52,7 +52,7 @@ const uiLanguages = [
{
"iso_code": "fr",
"self_name": "Français",
"translation_count": 57
"translation_count": 68
},
{
"iso_code": "ha",
@@ -62,22 +62,27 @@ const uiLanguages = [
{
"iso_code": "he",
"self_name": "עברית",
"translation_count": 57
"translation_count": 69
},
{
"iso_code": "hi",
"self_name": "हिन्दी",
"translation_count": 49
"translation_count": 59
},
{
"iso_code": "hy",
"self_name": "Հայերեն",
"translation_count": 15
"translation_count": 25
},
{
"iso_code": "ia",
"self_name": "interlingua",
"translation_count": 57
"translation_count": 67
},
{
"iso_code": "id",
"self_name": "Bahasa Inggris",
"translation_count": 68
},
{
"iso_code": "ig",
@@ -87,7 +92,7 @@ const uiLanguages = [
{
"iso_code": "it",
"self_name": "italiano",
"translation_count": 38
"translation_count": 54
},
{
"iso_code": "ja",
@@ -97,7 +102,7 @@ const uiLanguages = [
{
"iso_code": "ko",
"self_name": "한국어",
"translation_count": 15
"translation_count": 64
},
{
"iso_code": "ku-latn",
@@ -107,22 +112,27 @@ const uiLanguages = [
{
"iso_code": "lb",
"self_name": "Lëtzebuergesch",
"translation_count": 22
"translation_count": 43
},
{
"iso_code": "mk",
"self_name": "македонски",
"translation_count": 57
"translation_count": 76
},
{
"iso_code": "ms",
"self_name": "Bahasa Melayu",
"translation_count": 14
},
{
"iso_code": "nb",
"self_name": "Engelsk",
"translation_count": 50
},
{
"iso_code": "nl",
"self_name": "Nederlands",
"translation_count": 49
"translation_count": 68
},
{
"iso_code": "nqo",
@@ -137,17 +147,27 @@ const uiLanguages = [
{
"iso_code": "pl",
"self_name": "Polski",
"translation_count": 31
"translation_count": 32
},
{
"iso_code": "pt-br",
"self_name": "Português",
"translation_count": 35
"translation_count": 65
},
{
"iso_code": "pt",
"self_name": "português",
"translation_count": 67
},
{
"iso_code": "ro",
"self_name": "Engleză",
"translation_count": 67
},
{
"iso_code": "ru",
"self_name": "русский",
"translation_count": 57
"translation_count": 67
},
{
"iso_code": "sc",
@@ -162,7 +182,7 @@ const uiLanguages = [
{
"iso_code": "skr-arab",
"self_name": "سرائیکی",
"translation_count": 20
"translation_count": 31
},
{
"iso_code": "sl",
@@ -172,17 +192,17 @@ const uiLanguages = [
{
"iso_code": "sq",
"self_name": "Shqip",
"translation_count": 49
"translation_count": 67
},
{
"iso_code": "sv",
"self_name": "Svenska",
"translation_count": 57
"translation_count": 67
},
{
"iso_code": "sw",
"self_name": "Kiswahili",
"translation_count": 57
"translation_count": 58
},
{
"iso_code": "te",
@@ -197,11 +217,11 @@ const uiLanguages = [
{
"iso_code": "zh-hans",
"self_name": "简体中文",
"translation_count": 57
"translation_count": 68
},
{
"iso_code": "zh-hant",
"self_name": "繁體中文",
"translation_count": 57
"translation_count": 76
}
]

View File

@@ -92,14 +92,17 @@ function makeJSLink(jsCodeString, linkText, linkAttr="") {
// in the JS code we have to URI-encode a second time.
// (see https://stackoverflow.com/questions/33721510)
const uriEncodedJSCode = encodeURIComponent(jsCodeString);
return `<a ${linkAttr} href="javascript:${uriEncodedJSCode}">${linkText}</a>`;
const linkPlainText = htmlDecode(linkText, "text/html");
linkAttr += ` href="javascript:${uriEncodedJSCode}"`;
linkAttr += ` title="${linkPlainText}"`;
return `<a ${linkAttr}>${linkText}</a>`;
}
function suggestionsApiURL()
{
const uriEncodedBookName = encodeURIComponent(currentBook);
const userLang = viewerState.uiLanguage;
return `${root}/suggest?userlang=${userLang}&content=${uriEncodedBookName}`;
return `${root}/suggest?userlang=${userLang}&mode=smart&content=${uriEncodedBookName}`;
}
function setTitle(element, text) {
@@ -183,9 +186,13 @@ function closeSuggestions() {
}
}
function setSearchQuery(text) {
document.getElementById("kiwixsearchbox").value = text;
}
function updateSearchBoxForLocationChange() {
closeSuggestions();
document.getElementById("kiwixsearchbox").value = getSearchPattern();
setSearchQuery(getSearchPattern());
}
function updateSearchBoxForBookChange() {
@@ -259,22 +266,7 @@ function handle_location_hash_change() {
}
function translateErrorPageIfNeeded() {
const cw = contentIframe.contentWindow;
if ( cw.KIWIX_RESPONSE_TEMPLATE && cw.KIWIX_RESPONSE_DATA ) {
const template = htmlDecode(cw.KIWIX_RESPONSE_TEMPLATE);
// cw.KIWIX_RESPONSE_DATA belongs to the iframe context and running
// I18n.render() on it directly in the top context doesn't work correctly
// because the type checks (obj.__proto__ == ???.prototype) in
// I18n.instantiateParameterizedMessages() always 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(cw.KIWIX_RESPONSE_DATA));
const html = I18n.render(template, params);
const htmlDoc = new DOMParser().parseFromString(html, "text/html");
cw.document.documentElement.innerHTML = htmlDoc.documentElement.innerHTML;
}
translatePageInWindow(contentIframe.contentWindow);
}
function handle_content_url_change() {
@@ -310,6 +302,12 @@ function blockLink(url) {
: url;
}
function urlMustBeHandledByAnExternalApp(url) {
const WHITELISTED_URL_SCHEMATA = ['http:', 'https:', 'about:', 'javascript:'];
return WHITELISTED_URL_SCHEMATA.indexOf(url.protocol) == -1;
}
function isExternalUrl(url) {
if ( url.startsWith(window.location.origin) )
return false;
@@ -329,20 +327,46 @@ function getRealHref(target) {
return target_href;
}
function setHrefAvoidingWombatRewriting(target, url) {
const old_no_rewrite = target._no_rewrite;
target._no_rewrite = true;
target.setAttribute("href", url);
target._no_rewrite = old_no_rewrite;
}
function linkShouldBeOpenedInANewWindow(linkElement, mouseEvent) {
return linkElement.getAttribute("target") == "_blank"
|| mouseEvent.ctrlKey
|| mouseEvent.shiftKey
|| mouseEvent.metaKey /* on Macs */;
}
function goingToOpenALinkToAnUndisplayableResource(url) {
return !navigator.pdfViewerEnabled && url.pathname.endsWith('.pdf');
}
function onClickEvent(e) {
const iframeDocument = contentIframe.contentDocument;
const target = matchingAncestorElement(e.target, iframeDocument, "a");
if (target !== null && "href" in target) {
const target_href = getRealHref(target);
if (isExternalUrl(target_href)) {
const target_url = new URL(target_href, iframeDocument.location);
const isExternalAppUrl = urlMustBeHandledByAnExternalApp(target_url);
if ( (isExternalAppUrl && !viewerSettings.linkBlockingEnabled)
|| goingToOpenALinkToAnUndisplayableResource(target_url) ) {
target.setAttribute("target", "_blank");
}
if (isExternalAppUrl || isExternalUrl(target_href)) {
const possiblyBlockedLink = blockLink(target_href);
if ( e.ctrlKey || e.shiftKey ) {
if ( linkShouldBeOpenedInANewWindow(target, e) ) {
// The link will be loaded in a new tab/window - update the link
// and let the browser handle the rest.
target.setAttribute("href", possiblyBlockedLink);
setHrefAvoidingWombatRewriting(target, possiblyBlockedLink);
} else {
// Load the external URL in the viewer window (rather than iframe)
contentIframe.contentWindow.parent.location = possiblyBlockedLink;
e.preventDefault();
}
}
}
@@ -389,6 +413,10 @@ function setup_external_link_blocker() {
let viewerSetupComplete = false;
function on_content_load() {
const loader = document.getElementById("kiwix__loader");
contentIframe.classList.remove("hidden");
loader.style.display = "none";
if ( viewerSetupComplete ) {
handle_content_url_change();
}
@@ -440,7 +468,8 @@ function setupSuggestions() {
resultItem: {
element: (item, data) => {
const uriEncodedBookName = encodeURIComponent(currentBook);
let url;
const linkText = htmlDecode(data.value.label);
let url, modifiedQuery;
if (data.value.kind == "path") {
// The double quote and backslash symbols are included in the list
// of special symbols to URI-encode so that the resulting URL can
@@ -449,15 +478,18 @@ function setupSuggestions() {
const path = htmlDecode(data.value.path);
const quasiUriEncodedPath = quasiUriEncode(path, '#?"\\');
url = `/content/${uriEncodedBookName}/${quasiUriEncodedPath}`;
} else {
} else if (data.value.kind == "pattern") {
const pattern = encodeURIComponent(htmlDecode(data.value.value));
url = `/search?content=${uriEncodedBookName}&pattern=${pattern}`;
} else { // data.value.kind == "modifiedquery"
modifiedQuery = htmlDecode(linkText);
}
// url can't contain any double quote and/or backslash symbols
// since they should have been URI-encoded. Therefore putting it
// inside double quotes should result in valid javascript.
const jsAction = `gotoUrl("${url}")`;
const linkText = htmlDecode(data.value.label);
const jsAction = url
? `gotoUrl("${url}")`
: `setSearchQuery("${modifiedQuery}")`;
item.innerHTML = makeJSLink(jsAction, linkText, 'class="suggest"');
},
highlight: "autoComplete_highlight",
@@ -577,6 +609,12 @@ function updateUIText() {
setTitle(document.getElementById("kiwix_serve_taskbar_random_button"),
$t("random-page-button-text"));
// Add translation for loading text as soon as translations are available
const loadingTextElement = document.getElementById("kiwix__loading_text");
if (loadingTextElement) {
loadingTextElement.textContent = $t("text-loading-content");
}
}
function finishViewerSetupOnceTranslationsAreLoaded()

View File

@@ -1,14 +1,32 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8" />
<title>External link blocked</title>
</head>
<body class="kiwix">
<h1>External link blocked</h1>
<p>This instance of Kiwix protects you from accidentally going to external (out-of ZIM) links.</p>
<p>If you intend to go to such locations, please click the link below.</p>
<p><a href="{{ source }}">Go to {{ source }}</a></p>
<div id="kiwixfooter">Powered by <a href="https://kiwix.org">Kiwix</a></div>
</body>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>{{external_link_detected}}</title>
<link type="text/css" href="{{root}}/skin/error.css?KIWIXCACHEID" rel="Stylesheet" />
<script type="module" src="{{root}}/skin/i18n.js?KIWIXCACHEID"></script>
<script>
window.KIWIX_RESPONSE_TEMPLATE = "{{KIWIX_RESPONSE_TEMPLATE}}";
window.KIWIX_RESPONSE_DATA = {{{KIWIX_RESPONSE_DATA}}};
</script>
</head>
<body>
<header>
<img src="{{root}}/skin/blocklink.svg?KIWIXCACHEID"
alt="{{caution_warning}}"
aria-label="{{caution_warning}}"
title="{{caution_warning}}">
</header>
<section class="intro">
<h1>{{external_link_detected}}</h1>
<p>{{external_link_intro}}</p>
<p><a href="{{url}}">{{ url }}</a></p>
</section>
<section class="advice">
<p>{{advice.p1}}</p>
<p>{{advice.p2}}</p>
<p>{{advice.p3}}</p>
</section>
</body>
</html>

View File

@@ -50,20 +50,11 @@
pointer-events: none;
}
.book__link__wrapper {
grid-column: 1 / 3;
grid-row: 1 / 3;
}
.book__link {
grid-row: 2 / 3;
}
.kiwixHomeBody__results {
flex-basis: 100%;
}
#book__title>a, .book__download a {
#book__title>a {
text-decoration: none;
all: unset;
}
@@ -117,25 +108,32 @@
<h3 class="kiwixHomeBody__results">{{translations.count-of-matching-books}}</h3>
{{#books}}
<div class="book__wrapper">
<div class="book__link__wrapper">
<div class="book__icon" {{faviconAttr}}></div>
<div class="book__header">
<div id="book__title"><a href="{{root}}/content/{{id}}">{{title}}</a></div>
{{#downloadAvailable}}
<div class="book__download"><span><a href="{{root}}/nojs/download/{{id}}">{{translations.download}}</a></span></div>
{{/downloadAvailable}}
</div>
<a class="book__link" href="{{root}}/content/{{id}}" title="{{translations.preview-book}}" aria-label="{{translations.preview-book}}">
<div class="book__description" title="{{description}}">{{description}}</div>
</a>
</div>
<div class="book__languageTag" {{languageAttr}}>{{langCode}}</div>
<div class="book__tags"><div class="book__tags--wrapper">
{{#tagList}}
<span class="tag__link" aria-label='{{tag}}' title='{{tag}}'>{{tag}}</span>
{{/tagList}}
<a class="book__link" href="{{root}}/content/{{id}}" title="{{translations.preview-book}}" aria-label="{{translations.preview-book}}">
<div class="book__link__wrapper">
<div class="book__icon" {{faviconAttr}}></div>
<div class="book__header">
<div id="book__title">{{title}}</div>
</div>
</div>
<div class="book__description" title="{{description}}">{{description}}</div>
</div>
</a>
<div class="book__meta">
<div class="book__languageTag" title="{{langTag.langFullString}}" aria-label="{{langTag.langFullString}}">{{langTag.langShortString}}</div>
<div class="book__tags"><div class="book__tags--wrapper">
{{#tagList}}
<span class="tag__link" aria-label='{{tag}}' title='{{tag}}'>{{tag}}</span>
{{/tagList}}
</div>
</div>
</div>
{{#downloadAvailable}}
<div>
<a class="book__download" href="{{root}}/nojs/download/{{id}}">
<img src="{{root}}/skin/download-white.svg?KIWIXCACHEID">
<span>{{translations.download}}</span>
</a>
</div>
{{/downloadAvailable}}
</div>
{{/books}}
</div>

View File

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

View File

@@ -33,6 +33,10 @@
</head>
<body style="margin:0" onload="setupViewer()">
<div class="loader" id="kiwix__loader">
<div class="spinner"></div>
<span id="kiwix__loading_text">Loading content...</span>
</div>
<div class="kiwix" style="display:none" id="kiwixtoolbarwrapper">
<div id="kiwixtoolbar" class="ui-widget-header">
<div class="kiwix_centered">
@@ -78,7 +82,7 @@
</div>
</div>
<iframe id="content_iframe"
<iframe id="content_iframe" class="hidden"
referrerpolicy="no-referrer"
onload="on_content_load()"
src="./skin/blank.html?KIWIXCACHEID" title="ZIM content" width="100%"

View File

@@ -15,11 +15,19 @@ struct XMLDoc : pugi::xml_document
} // unnamed namespace
#if _WIN32
# define DATA_ABS_PATH "C:\\data\\zim"
# define ZARA_ABS_PATH "C:\\data\\zim\\zara.zim"
#else
# define DATA_ABS_PATH "/data/zim"
# define ZARA_ABS_PATH "/data/zim/zara.zim"
#endif
TEST(BookTest, updateFromXMLTest)
{
const XMLDoc xml(R"(
<book id="zara"
path="./zara.zim"
path="zara.zim"
url="https://who.org/zara.zim"
title="Catch an infection in 24 hours"
description="Complete guide to contagious diseases"
@@ -40,9 +48,9 @@ TEST(BookTest, updateFromXMLTest)
)");
kiwix::Book book;
book.updateFromXml(xml.child("book"), "/data/zim");
book.updateFromXml(xml.child("book"), DATA_ABS_PATH);
EXPECT_EQ(book.getPath(), "/data/zim/zara.zim");
EXPECT_EQ(book.getPath(), ZARA_ABS_PATH);
EXPECT_EQ(book.getUrl(), "https://who.org/zara.zim");
EXPECT_EQ(book.getTitle(), "Catch an infection in 24 hours");
EXPECT_EQ(book.getDescription(), "Complete guide to contagious diseases");

View File

@@ -4,7 +4,7 @@
path="./zimfile_raycharles.zim"
url="https://github.com/kiwix/libkiwix/raw/master/test/data/zimfile_raycharles.zim"
title="Ray Charles"
description="Wikipedia articles about Ray Charles"
description="Wikipedia articles about Ray Charles (not all of them but near to what an average newborn may find more than enough)"
language="eng"
creator="Wikipedia"
publisher="Kiwix"
@@ -22,7 +22,7 @@
path="./zimfile_raycharles_uncategorized.zim"
url="https://github.com/kiwix/libkiwix/raw/master/test/data/zimfile_raycharles_uncategorized.zim"
title="Ray (uncategorized) Charles"
description="No category is assigned to this library entry."
description="No category is assigned to this library entry (neither adj nor xor was considered a good option)"
language="rus,eng"
creator="Wikipedia"
publisher="Kiwix"
@@ -39,7 +39,7 @@
path="./zimfile&amp;other.zim"
url="https://github.com/kiwix/libkiwix/raw/master/test/data/zimfile%26other.zim"
title="Charles, Ray"
description="Wikipedia articles about Ray Charles"
description="Wikipedia articles about Ray Charles or why and when one should go to library"
language="fra"
creator="Wikipedia"
publisher="Kiwix"

View File

@@ -1,4 +1,4 @@
#include "../src/server/i18n.h"
#include "../src/server/i18n_utils.h"
#include "gtest/gtest.h"
using namespace kiwix;
@@ -48,3 +48,17 @@ TEST(ParameterizedMessage, messagesWithParameters)
EXPECT_EQ(msg.getText("test"), "Filter [I18N] by [TESTING] tag \"\"");
}
}
TEST(I18n, translateBookCategory)
{
EXPECT_EQ(translateBookCategory("en", "ted"), "Ted");
EXPECT_EQ(translateBookCategory("test", "ted"), "[I18N] Ted [TESTING]");
EXPECT_EQ(translateBookCategory("en", "stack_exchange"), "Stack Exchange");
EXPECT_EQ(translateBookCategory("test", "stack_exchange"), "[I18N] Stack Exchange [TESTING]");
// unknown categories are simply not translated
EXPECT_EQ(translateBookCategory("en", "Qwerty"), "Qwerty");
EXPECT_EQ(translateBookCategory("test", "Qwerty"), "Qwerty");
}

View File

@@ -267,11 +267,21 @@ const char * sampleOpdsStream = R"(
)";
#ifdef _WIN32
# define ZIMFILE_PATH ".\\zimfile.zim"
# define EXAMPLE_PATH ".\\example.zim"
# define LIBRARY_PATH ".\\test\\library.xml"
#else
# define ZIMFILE_PATH "./zimfile.zim"
# define EXAMPLE_PATH "./example.zim"
# define LIBRARY_PATH "./test/library.xml"
#endif
const char sampleLibraryXML[] = R"(
<library version="1.0">
<book
id="raycharles"
path="./zimfile.zim"
path=")" ZIMFILE_PATH R"("
url="https://github.com/kiwix/libkiwix/raw/master/test/data/zimfile.zim"
title="Ray Charles"
description="Wikipedia articles about Ray Charles"
@@ -287,7 +297,7 @@ const char sampleLibraryXML[] = R"(
></book>
<book
id="example"
path="./example.zim"
path=")" EXAMPLE_PATH R"("
title="An example ZIM archive"
description="An eXaMpLe book added to the catalog via XML"
language="deu"
@@ -383,7 +393,7 @@ class LibraryTest : public ::testing::Test {
void SetUp() override {
kiwix::Manager manager(lib);
manager.readOpds(sampleOpdsStream, "foo.urlHost");
manager.readXml(sampleLibraryXML, false, "./test/library.xml", true);
manager.readXml(sampleLibraryXML, false, LIBRARY_PATH, true);
}
kiwix::Bookmark createBookmark(const std::string &id, const std::string& url="", const std::string& title="") {

View File

@@ -103,7 +103,7 @@ std::string maskVariableOPDSFeedData(std::string s)
#define _CHARLES_RAY_CATALOG_ENTRY(CONTENT_NAME) CATALOG_ENTRY( \
"charlesray", \
"Charles, Ray", \
"Wikipedia articles about Ray Charles", \
"Wikipedia articles about Ray Charles or why and when one should go to library", \
"fra", \
"wikipedia_fr_ray_charles",\
"jazz",\
@@ -120,7 +120,7 @@ std::string maskVariableOPDSFeedData(std::string s)
#define _RAY_CHARLES_CATALOG_ENTRY(CONTENT_NAME) CATALOG_ENTRY(\
"raycharles",\
"Ray Charles",\
"Wikipedia articles about Ray Charles",\
"Wikipedia articles about Ray Charles (not all of them but near to what an average newborn may find more than enough)",\
"eng",\
"wikipedia_en_ray_charles",\
"wikipedia",\
@@ -139,7 +139,7 @@ std::string maskVariableOPDSFeedData(std::string s)
#define UNCATEGORIZED_RAY_CHARLES_CATALOG_ENTRY CATALOG_ENTRY(\
"raycharles_uncategorized",\
"Ray (uncategorized) Charles",\
"No category is assigned to this library entry.",\
"No category is assigned to this library entry (neither adj nor xor was considered a good option)",\
"rus,eng",\
"wikipedia_ru_ray_charles",\
"",\
@@ -199,8 +199,8 @@ TEST_F(LibraryServerTest, catalog_search_by_phrase)
" <startIndex>0</startIndex>\n"
" <itemsPerPage>2</itemsPerPage>\n"
CATALOG_LINK_TAGS
RAY_CHARLES_CATALOG_ENTRY
CHARLES_RAY_CATALOG_ENTRY
RAY_CHARLES_CATALOG_ENTRY
"</feed>\n"
);
}
@@ -218,8 +218,8 @@ TEST_F(LibraryServerTest, catalog_search_by_words)
" <startIndex>0</startIndex>\n"
" <itemsPerPage>3</itemsPerPage>\n"
CATALOG_LINK_TAGS
RAY_CHARLES_CATALOG_ENTRY
CHARLES_RAY_CATALOG_ENTRY
RAY_CHARLES_CATALOG_ENTRY
UNCATEGORIZED_RAY_CHARLES_CATALOG_ENTRY
"</feed>\n"
);
@@ -239,8 +239,8 @@ TEST_F(LibraryServerTest, catalog_prefix_search)
" <startIndex>0</startIndex>\n"
" <itemsPerPage>2</itemsPerPage>\n"
CATALOG_LINK_TAGS
RAY_CHARLES_CATALOG_ENTRY
CHARLES_RAY_CATALOG_ENTRY
RAY_CHARLES_CATALOG_ENTRY
"</feed>\n"
);
}
@@ -275,8 +275,8 @@ TEST_F(LibraryServerTest, catalog_search_with_word_exclusion)
" <startIndex>0</startIndex>\n"
" <itemsPerPage>2</itemsPerPage>\n"
CATALOG_LINK_TAGS
RAY_CHARLES_CATALOG_ENTRY
CHARLES_RAY_CATALOG_ENTRY
RAY_CHARLES_CATALOG_ENTRY
"</feed>\n"
);
}
@@ -331,8 +331,8 @@ TEST_F(LibraryServerTest, catalog_search_by_category)
" <startIndex>0</startIndex>\n"
" <itemsPerPage>2</itemsPerPage>\n"
CATALOG_LINK_TAGS
RAY_CHARLES_CATALOG_ENTRY
CHARLES_RAY_CATALOG_ENTRY
RAY_CHARLES_CATALOG_ENTRY
"</feed>\n"
);
}
@@ -772,10 +772,171 @@ TEST_F(LibraryServerTest, catalog_v2_entries_filtered_by_search_terms)
" <totalResults>2</totalResults>\n"
" <startIndex>0</startIndex>\n"
" <itemsPerPage>2</itemsPerPage>\n"
CHARLES_RAY_CATALOG_ENTRY
RAY_CHARLES_CATALOG_ENTRY
"</feed>\n"
);
}
TEST_F(LibraryServerTest, catalog_v2_entries_filtering_special_queries)
{
{
// 'or' doesn't act as a Xapian boolean operator
const auto r = zfs1_->GET("/ROOT%23%3F/catalog/v2/entries?q=Or");
EXPECT_EQ(r->status, 200);
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
CATALOG_V2_ENTRIES_PREAMBLE("?q=Or")
" <title>Filtered Entries (q=Or)</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <totalResults>1</totalResults>\n"
" <startIndex>0</startIndex>\n"
" <itemsPerPage>1</itemsPerPage>\n"
CHARLES_RAY_CATALOG_ENTRY
"</feed>\n"
);
}
{
// 'and' doesn't act as a Xapian boolean operator
const auto r = zfs1_->GET("/ROOT%23%3F/catalog/v2/entries?q=and");
EXPECT_EQ(r->status, 200);
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
CATALOG_V2_ENTRIES_PREAMBLE("?q=and")
" <title>Filtered Entries (q=and)</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <totalResults>1</totalResults>\n"
" <startIndex>0</startIndex>\n"
" <itemsPerPage>1</itemsPerPage>\n"
CHARLES_RAY_CATALOG_ENTRY
"</feed>\n"
);
}
{
// 'not' doesn't act as a Xapian boolean operator
const auto r = zfs1_->GET("/ROOT%23%3F/catalog/v2/entries?q=not");
EXPECT_EQ(r->status, 200);
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
CATALOG_V2_ENTRIES_PREAMBLE("?q=not")
" <title>Filtered Entries (q=not)</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <totalResults>1</totalResults>\n"
" <startIndex>0</startIndex>\n"
" <itemsPerPage>1</itemsPerPage>\n"
RAY_CHARLES_CATALOG_ENTRY
"</feed>\n"
);
}
{
// 'xor' doesn't act as a Xapian boolean operator
const auto r = zfs1_->GET("/ROOT%23%3F/catalog/v2/entries?q=xor");
EXPECT_EQ(r->status, 200);
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
CATALOG_V2_ENTRIES_PREAMBLE("?q=xor")
" <title>Filtered Entries (q=xor)</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <totalResults>1</totalResults>\n"
" <startIndex>0</startIndex>\n"
" <itemsPerPage>1</itemsPerPage>\n"
UNCATEGORIZED_RAY_CHARLES_CATALOG_ENTRY
"</feed>\n"
);
}
{
// 'or' acts as a Xapian boolean operator
const auto r = zfs1_->GET("/ROOT%23%3F/catalog/v2/entries?q=wikipedia%20or%20library");
EXPECT_EQ(r->status, 200);
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
CATALOG_V2_ENTRIES_PREAMBLE("?q=wikipedia%20or%20library")
" <title>Filtered Entries (q=wikipedia%20or%20library)</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <totalResults>1</totalResults>\n"
" <startIndex>0</startIndex>\n"
" <itemsPerPage>1</itemsPerPage>\n"
CHARLES_RAY_CATALOG_ENTRY
"</feed>\n"
);
}
{
// 'and' acts as a Xapian boolean operator
const auto r = zfs1_->GET("/ROOT%23%3F/catalog/v2/entries?q=wikipedia%20and%20articles");
EXPECT_EQ(r->status, 200);
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
CATALOG_V2_ENTRIES_PREAMBLE("?q=wikipedia%20and%20articles")
" <title>Filtered Entries (q=wikipedia%20and%20articles)</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <totalResults>1</totalResults>\n"
" <startIndex>0</startIndex>\n"
" <itemsPerPage>1</itemsPerPage>\n"
CHARLES_RAY_CATALOG_ENTRY
"</feed>\n"
);
}
{
// 'near' doesn't act as a Xapian query operator
const auto r = zfs1_->GET("/ROOT%23%3F/catalog/v2/entries?q=near");
EXPECT_EQ(r->status, 200);
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
CATALOG_V2_ENTRIES_PREAMBLE("?q=near")
" <title>Filtered Entries (q=near)</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <totalResults>1</totalResults>\n"
" <startIndex>0</startIndex>\n"
" <itemsPerPage>1</itemsPerPage>\n"
RAY_CHARLES_CATALOG_ENTRY
"</feed>\n"
);
}
{
// 'adj' doesn't act as a Xapian query operator
const auto r = zfs1_->GET("/ROOT%23%3F/catalog/v2/entries?q=adj");
EXPECT_EQ(r->status, 200);
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
CATALOG_V2_ENTRIES_PREAMBLE("?q=adj")
" <title>Filtered Entries (q=adj)</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <totalResults>1</totalResults>\n"
" <startIndex>0</startIndex>\n"
" <itemsPerPage>1</itemsPerPage>\n"
UNCATEGORIZED_RAY_CHARLES_CATALOG_ENTRY
"</feed>\n"
);
}
{
// 'near' doesn't act as a Xapian query operator
const auto r = zfs1_->GET("/ROOT%23%3F/catalog/v2/entries?q=charles%20near%20why");
EXPECT_EQ(r->status, 200);
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
CATALOG_V2_ENTRIES_PREAMBLE("?q=charles%20near%20why")
" <title>Filtered Entries (q=charles%20near%20why)</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <totalResults>0</totalResults>\n"
" <startIndex>0</startIndex>\n"
" <itemsPerPage>0</itemsPerPage>\n"
"</feed>\n"
);
}
{
// 'adj' doesn't act as a Xapian query operator
const auto r = zfs1_->GET("/ROOT%23%3F/catalog/v2/entries?q=charles%20adj%20why");
EXPECT_EQ(r->status, 200);
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
CATALOG_V2_ENTRIES_PREAMBLE("?q=charles%20adj%20why")
" <title>Filtered Entries (q=charles%20adj%20why)</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <totalResults>0</totalResults>\n"
" <startIndex>0</startIndex>\n"
" <itemsPerPage>0</itemsPerPage>\n"
"</feed>\n"
);
}
}
TEST_F(LibraryServerTest, catalog_v2_entries_filtered_by_language)
@@ -841,8 +1002,8 @@ TEST_F(LibraryServerTest, catalog_v2_entries_filtered_by_category)
" <totalResults>2</totalResults>\n"
" <startIndex>0</startIndex>\n"
" <itemsPerPage>2</itemsPerPage>\n"
RAY_CHARLES_CATALOG_ENTRY
CHARLES_RAY_CATALOG_ENTRY
RAY_CHARLES_CATALOG_ENTRY
"</feed>\n"
);
}
@@ -1028,12 +1189,12 @@ TEST_F(LibraryServerTest, no_name_mapper_catalog_v2_individual_entry_access)
" <title>Welcome to Kiwix Server</title>\n" \
" <link\n" \
" type=\"text/css\"\n" \
" href=\"/ROOT%23%3F/skin/kiwix.css?cacheid=2158fad9\"\n" \
" href=\"/ROOT%23%3F/skin/kiwix.css?cacheid=3948b846\"\n" \
" rel=\"Stylesheet\"\n" \
" />\n" \
" <link\n" \
" type=\"text/css\"\n" \
" href=\"/ROOT%23%3F/skin/index.css?cacheid=1e78e7cf\"\n" \
" href=\"/ROOT%23%3F/skin/index.css?cacheid=ae79e41a\"\n" \
" rel=\"Stylesheet\"\n" \
" />\n" \
" <link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"/ROOT%23%3F/skin/favicon/apple-touch-icon.png?cacheid=f86f8df3\">\n" \
@@ -1066,17 +1227,10 @@ TEST_F(LibraryServerTest, no_name_mapper_catalog_v2_individual_entry_access)
" .tag__link {\n" \
" pointer-events: none;\n" \
" }\n\n" \
" .book__link__wrapper {\n" \
" grid-column: 1 / 3;\n" \
" grid-row: 1 / 3;\n" \
" }\n\n" \
" .book__link {\n" \
" grid-row: 2 / 3;\n" \
" }\n\n" \
" .kiwixHomeBody__results {\n" \
" flex-basis: 100%;\n" \
" }\n\n" \
" #book__title>a, .book__download a {\n" \
" #book__title>a {\n" \
" text-decoration: none;\n" \
" all: unset;\n" \
" }\n" \
@@ -1087,62 +1241,83 @@ TEST_F(LibraryServerTest, no_name_mapper_catalog_v2_individual_entry_access)
#define CHARLES_RAY_BOOK_HTML \
" <div class=\"book__wrapper\">\n" \
" <div class=\"book__link__wrapper\">\n" \
" <div class=\"book__icon\" style=background-image:url(/ROOT%23%3F/catalog/v2/illustration/charlesray/?size=48)></div>\n" \
" <div class=\"book__header\">\n" \
" <div id=\"book__title\"><a href=\"/ROOT%23%3F/content/zimfile%26other\">Charles, Ray</a></div>\n" \
" <div class=\"book__download\"><span><a href=\"/ROOT%23%3F/nojs/download/zimfile%26other\">Download</a></span></div>\n" \
" </div>\n" \
" <a class=\"book__link\" href=\"/ROOT%23%3F/content/zimfile%26other\" title=\"Preview\" aria-label=\"Preview\">\n" \
" <div class=\"book__description\" title=\"Wikipedia articles about Ray Charles\">Wikipedia articles about Ray Charles</div>\n" \
" </a>\n" \
" </div>\n" \
" <div class=\"book__languageTag\" >fra</div>\n" \
" <div class=\"book__tags\"><div class=\"book__tags--wrapper\">\n" \
" <span class=\"tag__link\" aria-label='unittest' title='unittest'>unittest</span>\n" \
" <span class=\"tag__link\" aria-label='wikipedia' title='wikipedia'>wikipedia</span>\n" \
" <a class=\"book__link\" href=\"/ROOT%23%3F/content/zimfile%26other\" title=\"Preview\" aria-label=\"Preview\">\n" \
" <div class=\"book__link__wrapper\">\n" \
" <div class=\"book__icon\" style=background-image:url(/ROOT%23%3F/catalog/v2/illustration/charlesray/?size=48)></div>\n" \
" <div class=\"book__header\">\n" \
" <div id=\"book__title\">Charles, Ray</div>\n" \
" </div>\n" \
" <div class=\"book__description\" title=\"Wikipedia articles about Ray Charles or why and when one should go to library\">Wikipedia articles about Ray Charles or why and when one should go to library</div>\n" \
" </div>\n" \
" </a>\n" \
" <div class=\"book__meta\">\n" \
" <div class=\"book__languageTag\" title=\"français\" aria-label=\"français\">fra</div>\n" \
" <div class=\"book__tags\"><div class=\"book__tags--wrapper\">\n" \
" <span class=\"tag__link\" aria-label='unittest' title='unittest'>unittest</span>\n" \
" <span class=\"tag__link\" aria-label='wikipedia' title='wikipedia'>wikipedia</span>\n" \
" </div>\n" \
" </div>\n" \
" </div>\n" \
" <div>\n" \
" <a class=\"book__download\" href=\"/ROOT%23%3F/nojs/download/zimfile%26other\">\n" \
" <img src=\"/ROOT%23%3F/skin/download-white.svg?cacheid=079ab989\">\n" \
" <span>Download</span>\n" \
" </a>\n" \
" </div>\n" \
" </div>\n" \
" </div>\n"
#define RAY_CHARLES_BOOK_HTML \
" <div class=\"book__wrapper\">\n" \
" <div class=\"book__link__wrapper\">\n" \
" <div class=\"book__icon\" style=background-image:url(/ROOT%23%3F/catalog/v2/illustration/raycharles/?size=48)></div>\n" \
" <div class=\"book__header\">\n" \
" <div id=\"book__title\"><a href=\"/ROOT%23%3F/content/zimfile_raycharles\">Ray Charles</a></div>\n" \
" <div class=\"book__download\"><span><a href=\"/ROOT%23%3F/nojs/download/zimfile_raycharles\">Download</a></span></div>\n" \
" </div>\n" \
" <a class=\"book__link\" href=\"/ROOT%23%3F/content/zimfile_raycharles\" title=\"Preview\" aria-label=\"Preview\">\n" \
" <div class=\"book__description\" title=\"Wikipedia articles about Ray Charles\">Wikipedia articles about Ray Charles</div>\n" \
" </a>\n" \
" </div>\n" \
" <div class=\"book__languageTag\" >eng</div>\n" \
" <div class=\"book__tags\"><div class=\"book__tags--wrapper\">\n" \
" <span class=\"tag__link\" aria-label='public_tag_without_a_value' title='public_tag_without_a_value'>public_tag_without_a_value</span>\n" \
" <span class=\"tag__link\" aria-label='wikipedia' title='wikipedia'>wikipedia</span>\n" \
" <a class=\"book__link\" href=\"/ROOT%23%3F/content/zimfile_raycharles\" title=\"Preview\" aria-label=\"Preview\">\n" \
" <div class=\"book__link__wrapper\">\n" \
" <div class=\"book__icon\" style=background-image:url(/ROOT%23%3F/catalog/v2/illustration/raycharles/?size=48)></div>\n" \
" <div class=\"book__header\">\n" \
" <div id=\"book__title\">Ray Charles</div>\n" \
" </div>\n" \
" <div class=\"book__description\" title=\"Wikipedia articles about Ray Charles (not all of them but near to what an average newborn may find more than enough)\">Wikipedia articles about Ray Charles (not all of them but near to what an average newborn may find more than enough)</div>\n" \
" </div>\n" \
" </a>\n" \
" <div class=\"book__meta\">\n" \
" <div class=\"book__languageTag\" title=\"English\" aria-label=\"English\">eng</div>\n" \
" <div class=\"book__tags\"><div class=\"book__tags--wrapper\">\n" \
" <span class=\"tag__link\" aria-label='public_tag_without_a_value' title='public_tag_without_a_value'>public_tag_without_a_value</span>\n" \
" <span class=\"tag__link\" aria-label='wikipedia' title='wikipedia'>wikipedia</span>\n" \
" </div>\n" \
" </div>\n" \
" </div>\n" \
" <div>\n" \
" <a class=\"book__download\" href=\"/ROOT%23%3F/nojs/download/zimfile_raycharles\">\n" \
" <img src=\"/ROOT%23%3F/skin/download-white.svg?cacheid=079ab989\">\n" \
" <span>Download</span>\n" \
" </a>\n" \
" </div>\n" \
" </div>\n" \
" </div>\n"
#define RAY_CHARLES_UNCTZ_BOOK_HTML \
" <div class=\"book__wrapper\">\n" \
" <div class=\"book__link__wrapper\">\n" \
" <div class=\"book__icon\" style=background-image:url(/ROOT%23%3F/catalog/v2/illustration/raycharles_uncategorized/?size=48)></div>\n" \
" <div class=\"book__header\">\n" \
" <div id=\"book__title\"><a href=\"/ROOT%23%3F/content/zimfile_raycharles_uncategorized\">Ray (uncategorized) Charles</a></div>\n" \
" <div class=\"book__download\"><span><a href=\"/ROOT%23%3F/nojs/download/zimfile_raycharles_uncategorized\">Download</a></span></div>\n" \
" </div>\n" \
" <a class=\"book__link\" href=\"/ROOT%23%3F/content/zimfile_raycharles_uncategorized\" title=\"Preview\" aria-label=\"Preview\">\n" \
" <div class=\"book__description\" title=\"No category is assigned to this library entry.\">No category is assigned to this library entry.</div>\n" \
" </a>\n" \
" </div>\n" \
" <div class=\"book__languageTag\" >rus,eng</div>\n" \
" <div class=\"book__tags\"><div class=\"book__tags--wrapper\">\n" \
" <span class=\"tag__link\" aria-label='public_tag_with_a_value:value_of_a_public_tag' title='public_tag_with_a_value:value_of_a_public_tag'>public_tag_with_a_value:value_of_a_public_tag</span>\n" \
" <span class=\"tag__link\" aria-label='wikipedia' title='wikipedia'>wikipedia</span>\n" \
" <a class=\"book__link\" href=\"/ROOT%23%3F/content/zimfile_raycharles_uncategorized\" title=\"Preview\" aria-label=\"Preview\">\n" \
" <div class=\"book__link__wrapper\">\n" \
" <div class=\"book__icon\" style=background-image:url(/ROOT%23%3F/catalog/v2/illustration/raycharles_uncategorized/?size=48)></div>\n" \
" <div class=\"book__header\">\n" \
" <div id=\"book__title\">Ray (uncategorized) Charles</div>\n" \
" </div>\n" \
" <div class=\"book__description\" title=\"No category is assigned to this library entry (neither adj nor xor was considered a good option)\">No category is assigned to this library entry (neither adj nor xor was considered a good option)</div>\n" \
" </div>\n" \
" </a>\n" \
" <div class=\"book__meta\">\n" \
" <div class=\"book__languageTag\" title=\"русский,English\" aria-label=\"русский,English\">mul</div>\n" \
" <div class=\"book__tags\"><div class=\"book__tags--wrapper\">\n" \
" <span class=\"tag__link\" aria-label='public_tag_with_a_value:value_of_a_public_tag' title='public_tag_with_a_value:value_of_a_public_tag'>public_tag_with_a_value:value_of_a_public_tag</span>\n" \
" <span class=\"tag__link\" aria-label='wikipedia' title='wikipedia'>wikipedia</span>\n" \
" </div>\n" \
" </div>\n" \
" </div>\n" \
" <div>\n" \
" <a class=\"book__download\" href=\"/ROOT%23%3F/nojs/download/zimfile_raycharles_uncategorized\">\n" \
" <img src=\"/ROOT%23%3F/skin/download-white.svg?cacheid=079ab989\">\n" \
" <span>Download</span>\n" \
" </a>\n" \
" </div>\n" \
" </div>\n" \
" </div>\n"
#define FINAL_HTML_TEXT \

View File

@@ -25,11 +25,23 @@ TEST(ManagerTest, addBookFromPathAndGetIdTest)
EXPECT_EQ(book.getUrl(), url);
}
#if _WIN32
# define UNITTEST_ZIM_PATH "zimfiles\\unittest.zim"
# define LIB_ABS_PATH "C:\\data\\lib.xml"
# define ZIM_ABS_PATH "C:\\data\\zimfiles\\unittest.zim"
#else
# define UNITTEST_ZIM_PATH "zimfiles/unittest.zim"
# define LIB_ABS_PATH "/data/lib.xml"
# define ZIM_ABS_PATH "/data/zimfiles/unittest.zim"
#endif
const char sampleLibraryXML[] = R"(
<library version="1.0">
<book
id="0d0bcd57-d3f6-cb22-44cc-a723ccb4e1b2"
path="zimfiles/unittest.zim"
path=")" UNITTEST_ZIM_PATH R"("
url="https://example.com/zimfiles/unittest.zim"
title="Unit Test"
description="Wikipedia articles about unit testing"
@@ -51,9 +63,9 @@ TEST(ManagerTest, readXml)
auto lib = kiwix::Library::create();
kiwix::Manager manager = kiwix::Manager(lib);
EXPECT_EQ(true, manager.readXml(sampleLibraryXML, true, "/data/lib.xml", true));
EXPECT_EQ(true, manager.readXml(sampleLibraryXML, true, LIB_ABS_PATH, true));
kiwix::Book book = lib->getBookById("0d0bcd57-d3f6-cb22-44cc-a723ccb4e1b2");
EXPECT_EQ("/data/zimfiles/unittest.zim", book.getPath());
EXPECT_EQ(ZIM_ABS_PATH, book.getPath());
EXPECT_EQ("https://example.com/zimfiles/unittest.zim", book.getUrl());
EXPECT_EQ("Unit Test", book.getTitle());
EXPECT_EQ("Wikipedia articles about unit testing", book.getDescription());

View File

@@ -7,6 +7,23 @@
namespace
{
#if _WIN32
const char libraryXML[] = R"(
<library version="1.0">
<book id="01" path="C:\data\zero_one.zim"> </book>
<book id="02" path="C:\data\zero two.zim"> </book>
<book id="03" path="C:\data\ZERO thrêë.zim"> </book>
<book id="04-2021-10" path="C:\data\zero_four_2021-10.zim"></book>
<book id="04-2021-11" path="C:\data\zero_four_2021-11.zim"></book>
<book id="05-a" path="C:\data\zero_five-a.zim" name="zero_five"></book>
<book id="05-b" path="C:\data\zero_five-b.zim" name="zero_five"></book>
<book id="06+" path="C:\data\zërô + SIX.zim"></book>
<book id="06plus" path="C:\data\zero_plus_six.zim"></book>
<book id="07-super" path="C:\data\zero_seven.zim"></book>
<book id="07-sub" path="C:\data\subdir\zero_seven.zim"></book>
</library>
)";
#else
const char libraryXML[] = R"(
<library version="1.0">
<book id="01" path="/data/zero_one.zim"> </book>
@@ -22,6 +39,7 @@ const char libraryXML[] = R"(
<book id="07-sub" path="/data/subdir/zero_seven.zim"></book>
</library>
)";
#endif
class NameMapperTest : public ::testing::Test {
public:
@@ -61,7 +79,22 @@ public:
operator std::string() const { return buffer.str(); }
};
#if _WIN32
const std::string ZERO_FOUR_NAME_CONFLICT_MSG =
"Path collision: 'C:\\data\\zero_four_2021-10.zim' and"
" 'C:\\data\\zero_four_2021-11.zim' can't share the same URL path 'zero_four'."
" Therefore, only 'C:\\data\\zero_four_2021-10.zim' will be served.\n";
const std::string ZERO_SIX_NAME_CONFLICT_MSG =
"Path collision: 'C:\\data\\zërô + SIX.zim' and "
"'C:\\data\\zero_plus_six.zim' can't share the same URL path 'zero_plus_six'."
" Therefore, only 'C:\\data\\zërô + SIX.zim' will be served.\n";
const std::string ZERO_SEVEN_NAME_CONFLICT_MSG =
"Path collision: 'C:\\data\\subdir\\zero_seven.zim' and"
" 'C:\\data\\zero_seven.zim' can't share the same URL path 'zero_seven'."
" Therefore, only 'C:\\data\\subdir\\zero_seven.zim' will be served.\n";
#else
const std::string ZERO_FOUR_NAME_CONFLICT_MSG =
"Path collision: '/data/zero_four_2021-10.zim' and"
" '/data/zero_four_2021-11.zim' can't share the same URL path 'zero_four'."
@@ -76,6 +109,7 @@ const std::string ZERO_SEVEN_NAME_CONFLICT_MSG =
"Path collision: '/data/subdir/zero_seven.zim' and"
" '/data/zero_seven.zim' can't share the same URL path 'zero_seven'."
" Therefore, only '/data/subdir/zero_seven.zim' will be served.\n";
#endif
// Name conflicts in the default mode (without the --nodatealiases is off
const std::string DEFAULT_NAME_CONFLICTS = ZERO_SIX_NAME_CONFLICT_MSG

View File

@@ -18,9 +18,10 @@
*/
#include "gtest/gtest.h"
#include "../include/tools.h"
#include "../src/tools/otherTools.h"
#include "zim/suggestion_iterator.h"
#include "../src/server/i18n.h"
#include "../src/server/i18n_utils.h"
#include <regex>
@@ -252,11 +253,8 @@ TEST(networkTools, getNetworkInterfaces)
}
}
TEST(networkTools, getBestPublicIp)
TEST(networkTools, getBestPublicIps)
{
using kiwix::getBestPublicIp;
std::cout << "getBestPublicIp(true) " << getBestPublicIp(true) << std::endl;
std::cout << "getBestPublicIp(false) " << getBestPublicIp(false) << std::endl;
std::cout << "getBestPublicIp() " << getBestPublicIp() << std::endl;
std::cout << "getBestPublicIps(): " << "[" << kiwix::getBestPublicIps().addr << ", " << kiwix::getBestPublicIps().addr6 << "]" << std::endl;
std::cout << "getBestPublicIp(): " << kiwix::getBestPublicIp() << std::endl;
}

View File

@@ -57,25 +57,27 @@ const ResourceCollection resources200Compressible{
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/autoComplete/autoComplete.min.js" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/autoComplete/autoComplete.min.js?cacheid=1191aaaf" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/autoComplete/css/autoComplete.css" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/autoComplete/css/autoComplete.css?cacheid=ef30cd42" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/autoComplete/css/autoComplete.css?cacheid=f2d376c4" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/error.css" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/error.css?cacheid=b3fa90cf" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/i18n.js" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/i18n.js?cacheid=071abc9a" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/i18n.js?cacheid=e9a10ac1" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/index.css" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/index.css?cacheid=1e78e7cf" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/index.css?cacheid=ae79e41a" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/index.js" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/index.js?cacheid=f43eb0b9" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/index.js?cacheid=cc456f1f" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/iso6391To3.js" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/iso6391To3.js?cacheid=ecde2bb3" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/isotope.pkgd.min.js" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/isotope.pkgd.min.js?cacheid=2e48d392" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/kiwix.css" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/kiwix.css?cacheid=2158fad9" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/kiwix.css?cacheid=3948b846" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/mustache.min.js" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/mustache.min.js?cacheid=bd23c4fb" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/taskbar.css" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/taskbar.css?cacheid=80d56607" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/viewer.js" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/viewer.js?cacheid=5fc4badf" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/viewer.js?cacheid=f8c5f4bf" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/fonts/Poppins.ttf" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/fonts/Poppins.ttf?cacheid=af705837" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/fonts/Roboto.ttf" },
@@ -84,7 +86,7 @@ const ResourceCollection resources200Compressible{
// TODO: implement cache management of i18n resources
//{ STATIC_CONTENT, "/ROOT%23%3F/skin/i18n/test.json?cacheid=unknown" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/languages.js" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/languages.js?cacheid=ee7d95b5" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/languages.js?cacheid=a83f0e13" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/catalog/search" },
@@ -92,6 +94,8 @@ const ResourceCollection resources200Compressible{
{ DYNAMIC_CONTENT, "/ROOT%23%3F/catalog/v2/entries" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/catalog/v2/partial_entries" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/catch/external?source=www.example.com" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/search?content=zimfile&pattern=a" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/suggest?content=zimfile&term=ray" },
@@ -106,14 +110,20 @@ const ResourceCollection resources200Compressible{
};
const ResourceCollection resources200Uncompressible{
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/404.svg" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/404.svg?cacheid=b6d648af" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/bittorrent.png" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/bittorrent.png?cacheid=4f5c6882" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/blank.html" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/blank.html?cacheid=6b1fa032" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/blocklink.svg" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/blocklink.svg?cacheid=bd56b116" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/caret.png" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/caret.png?cacheid=22b942b4" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/download.png" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/download.png?cacheid=a39aa502" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/download-white.svg" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/download-white.svg?cacheid=079ab989"},
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/favicon/android-chrome-192x192.png" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/favicon/android-chrome-192x192.png?cacheid=bfac158b" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/favicon/android-chrome-512x512.png" },
@@ -170,8 +180,6 @@ const ResourceCollection resources200Uncompressible{
{ DYNAMIC_CONTENT, "/ROOT%23%3F/catalog/v2/searchdescription.xml" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/catalog/v2/illustration/6f1d19d0-633f-087b-fb55-7ac324ff9baf?size=48" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/catch/external?source=www.example.com" },
{ ZIM_CONTENT, "/ROOT%23%3F/content/zimfile/I/m/Ray_Charles_classic_piano_pose.jpg" },
{ ZIM_CONTENT, "/ROOT%23%3F/content/corner_cases%23%26/empty.html" },
@@ -278,8 +286,8 @@ TEST_F(ServerTest, CacheIdsOfStaticResources)
const std::vector<UrlAndExpectedResult> testData{
{
/* url */ "/ROOT%23%3F/",
R"EXPECTEDRESULT( href="/ROOT%23%3F/skin/kiwix.css?cacheid=2158fad9"
href="/ROOT%23%3F/skin/index.css?cacheid=1e78e7cf"
R"EXPECTEDRESULT( href="/ROOT%23%3F/skin/kiwix.css?cacheid=3948b846"
href="/ROOT%23%3F/skin/index.css?cacheid=ae79e41a"
<link rel="apple-touch-icon" sizes="180x180" href="/ROOT%23%3F/skin/favicon/apple-touch-icon.png?cacheid=f86f8df3">
<link rel="icon" type="image/png" sizes="32x32" href="/ROOT%23%3F/skin/favicon/favicon-32x32.png?cacheid=79ded625">
<link rel="icon" type="image/png" sizes="16x16" href="/ROOT%23%3F/skin/favicon/favicon-16x16.png?cacheid=a986fedc">
@@ -288,11 +296,11 @@ R"EXPECTEDRESULT( href="/ROOT%23%3F/skin/kiwix.css?cacheid=2158fad9"
<link rel="shortcut icon" href="/ROOT%23%3F/skin/favicon/favicon.ico?cacheid=92663314">
<meta name="msapplication-config" content="/ROOT%23%3F/skin/favicon/browserconfig.xml?cacheid=f29a7c4a">
<script type="text/javascript" src="./skin/polyfills.js?cacheid=a0e0343d"></script>
<script type="module" src="/ROOT%23%3F/skin/i18n.js?cacheid=071abc9a" defer></script>
<script type="text/javascript" src="/ROOT%23%3F/skin/languages.js?cacheid=ee7d95b5" defer></script>
<script type="module" src="/ROOT%23%3F/skin/i18n.js?cacheid=e9a10ac1" defer></script>
<script type="text/javascript" src="/ROOT%23%3F/skin/languages.js?cacheid=a83f0e13" defer></script>
<script src="/ROOT%23%3F/skin/isotope.pkgd.min.js?cacheid=2e48d392" defer></script>
<script src="/ROOT%23%3F/skin/iso6391To3.js?cacheid=ecde2bb3"></script>
<script type="text/javascript" src="/ROOT%23%3F/skin/index.js?cacheid=f43eb0b9" defer></script>
<script type="text/javascript" src="/ROOT%23%3F/skin/index.js?cacheid=cc456f1f" defer></script>
<img src="/ROOT%23%3F/skin/feed.svg?cacheid=055b333f"
<img src="/ROOT%23%3F/skin/langSelector.svg?cacheid=00b59961"
)EXPECTEDRESULT"
@@ -310,7 +318,8 @@ R"EXPECTEDRESULT( background-image: url('../skin/search-icon.svg?cacheid=b10a
},
{
/* url */ "/ROOT%23%3F/skin/index.js",
R"EXPECTEDRESULT( <img src="${root}/skin/download.png?cacheid=a39aa502" alt="${$t("direct-download-alt-text")}" />
R"EXPECTEDRESULT( <img src="${root}/skin/download-white.svg?cacheid=079ab989">
<img src="${root}/skin/download.png?cacheid=a39aa502" alt="${$t("direct-download-alt-text")}" />
<img src="${root}/skin/hash.png?cacheid=f836e872" alt="${$t("hash-download-alt-text")}" />
<img src="${root}/skin/magnet.png?cacheid=73b6bddf" alt="${$t("magnet-alt-text")}" />
<img src="${root}/skin/bittorrent.png?cacheid=4f5c6882" alt="${$t("torrent-download-alt-text")}" />
@@ -318,13 +327,13 @@ R"EXPECTEDRESULT( <img src="${root}/skin/download
},
{
/* url */ "/ROOT%23%3F/viewer",
R"EXPECTEDRESULT( <link type="text/css" href="./skin/kiwix.css?cacheid=2158fad9" rel="Stylesheet" />
R"EXPECTEDRESULT( <link type="text/css" href="./skin/kiwix.css?cacheid=3948b846" rel="Stylesheet" />
<link type="text/css" href="./skin/taskbar.css?cacheid=80d56607" rel="Stylesheet" />
<link type="text/css" href="./skin/autoComplete/css/autoComplete.css?cacheid=ef30cd42" rel="Stylesheet" />
<link type="text/css" href="./skin/autoComplete/css/autoComplete.css?cacheid=f2d376c4" rel="Stylesheet" />
<script type="text/javascript" src="./skin/polyfills.js?cacheid=a0e0343d"></script>
<script type="module" src="./skin/i18n.js?cacheid=071abc9a" defer></script>
<script type="text/javascript" src="./skin/languages.js?cacheid=ee7d95b5" defer></script>
<script type="text/javascript" src="./skin/viewer.js?cacheid=5fc4badf" defer></script>
<script type="module" src="./skin/i18n.js?cacheid=e9a10ac1" defer></script>
<script type="text/javascript" src="./skin/languages.js?cacheid=a83f0e13" defer></script>
<script type="text/javascript" src="./skin/viewer.js?cacheid=f8c5f4bf" defer></script>
<script type="text/javascript" src="./skin/autoComplete/autoComplete.min.js?cacheid=1191aaaf"></script>
const blankPageUrl = root + "/skin/blank.html?cacheid=6b1fa032";
<label for="kiwix_button_show_toggle"><img src="./skin/caret.png?cacheid=22b942b4" alt=""></label>
@@ -336,6 +345,21 @@ R"EXPECTEDRESULT( <link type="text/css" href="./skin/kiwix.css?cacheid=2158fa
/* url */ "/ROOT%23%3F/content/zimfile/A/index",
""
},
{
/* url */ "/ROOT%23%3F/content/invalid-book/whatever",
R"EXPECTEDRESULT( <link type="text/css" href="/ROOT%23%3F/skin/error.css?cacheid=b3fa90cf" rel="Stylesheet" />
window.KIWIX_RESPONSE_TEMPLATE = "&lt;!DOCTYPE html&gt;\n&lt;html&gt;\n &lt;head&gt;\n &lt;meta charset=&quot;utf-8&quot;&gt;\n &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no&quot; /&gt;\n &lt;title&gt;{{PAGE_TITLE}}&lt;/title&gt;\n &lt;link type=&quot;text/css&quot; href=&quot;{{root}}/skin/error.css?cacheid=b3fa90cf&quot; rel=&quot;Stylesheet&quot; /&gt;\n{{#KIWIX_RESPONSE_DATA}} &lt;script&gt;\n window.KIWIX_RESPONSE_TEMPLATE = &quot;{{KIWIX_RESPONSE_TEMPLATE}}&quot;;\n window.KIWIX_RESPONSE_DATA = {{{KIWIX_RESPONSE_DATA}}};\n &lt;/script&gt;{{/KIWIX_RESPONSE_DATA}}\n &lt;/head&gt;\n &lt;body&gt;\n &lt;header&gt;\n &lt;img src=&quot;{{root}}/skin/404.svg?cacheid=b6d648af&quot;\n alt=&quot;{{404_img_text}}&quot;\n aria-label=&quot;{{404_img_text}}&quot;\n title=&quot;{{404_img_text}}&quot;&gt;\n &lt;/header&gt;\n &lt;section class=&quot;intro&quot;&gt;\n &lt;h1&gt;{{PAGE_HEADING}}&lt;/h1&gt;\n &lt;p&gt;{{path_was_not_found_msg}}&lt;/p&gt;\n &lt;p&gt;&lt;code&gt;{{url_path}}&lt;/code&gt;&lt;/p&gt;\n &lt;/section&gt;\n &lt;section class=&quot;advice&quot;&gt;\n &lt;p&gt;{{advice.p1}}&lt;/p&gt;\n &lt;p class=&quot;list-intro&quot;&gt;{{advice.p2}}&lt;/p&gt;\n &lt;ul&gt;\n &lt;li&gt;{{advice.p3}}&lt;/li&gt;\n &lt;li&gt;{{advice.p4}}&lt;/li&gt;\n &lt;/ul&gt;\n &lt;p&gt;{{advice.p5}}&lt;/p&gt;\n &lt;/section&gt;\n &lt;/body&gt;\n&lt;/html&gt;\n";
<img src="/ROOT%23%3F/skin/404.svg?cacheid=b6d648af"
)EXPECTEDRESULT"
},
{
/* url */ "/ROOT%23%3F/catch/external?source=https%3A%2F%2Fkiwix.org",
R"EXPECTEDRESULT( <link type="text/css" href="/ROOT%23%3F/skin/error.css?cacheid=b3fa90cf" rel="Stylesheet" />
<script type="module" src="/ROOT%23%3F/skin/i18n.js?cacheid=e9a10ac1"></script>
window.KIWIX_RESPONSE_TEMPLATE = "&lt;!DOCTYPE html&gt;\n&lt;html&gt;\n &lt;head&gt;\n &lt;meta charset=&quot;utf-8&quot;&gt;\n &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no&quot; /&gt;\n &lt;title&gt;{{external_link_detected}}&lt;/title&gt;\n &lt;link type=&quot;text/css&quot; href=&quot;{{root}}/skin/error.css?cacheid=b3fa90cf&quot; rel=&quot;Stylesheet&quot; /&gt;\n &lt;script type=&quot;module&quot; src=&quot;{{root}}/skin/i18n.js?cacheid=e9a10ac1&quot;&gt;&lt;/script&gt;\n &lt;script&gt;\n window.KIWIX_RESPONSE_TEMPLATE = &quot;{{KIWIX_RESPONSE_TEMPLATE}}&quot;;\n window.KIWIX_RESPONSE_DATA = {{{KIWIX_RESPONSE_DATA}}};\n &lt;/script&gt;\n &lt;/head&gt;\n &lt;body&gt;\n &lt;header&gt;\n &lt;img src=&quot;{{root}}/skin/blocklink.svg?cacheid=bd56b116&quot;\n alt=&quot;{{caution_warning}}&quot;\n aria-label=&quot;{{caution_warning}}&quot;\n title=&quot;{{caution_warning}}&quot;&gt;\n &lt;/header&gt;\n &lt;section class=&quot;intro&quot;&gt;\n &lt;h1&gt;{{external_link_detected}}&lt;/h1&gt;\n &lt;p&gt;{{external_link_intro}}&lt;/p&gt;\n &lt;p&gt;&lt;a href=&quot;{{url}}&quot;&gt;{{ url }}&lt;/a&gt;&lt;/p&gt;\n &lt;/section&gt;\n &lt;section class=&quot;advice&quot;&gt;\n &lt;p&gt;{{advice.p1}}&lt;/p&gt;\n &lt;p&gt;{{advice.p2}}&lt;/p&gt;\n &lt;p&gt;{{advice.p3}}&lt;/p&gt;\n &lt;/section&gt;\n &lt;/body&gt;\n&lt;/html&gt;\n";
<img src="/ROOT%23%3F/skin/blocklink.svg?cacheid=bd56b116"
)EXPECTEDRESULT"
},
{
// Searching in a ZIM file without a full-text index returns
// a page rendered from static/templates/no_search_result_html
@@ -356,6 +380,57 @@ R"EXPECTEDRESULT( <link type="text/css" href="/ROOT%23%3F/skin/search_results
}
}
std::string getCacheIdFromUrl(const std::string& url)
{
const std::string q("?cacheid=");
const auto i = url.find(q);
return i == std::string::npos ? "" : url.substr(i + q.size());
}
std::string runExternalCmdAndGetItsOutput(const std::string& cmd)
{
std::string cmdOutput;
#ifdef _WIN32
#define popen _popen
#define pclose _pclose
#endif
if (FILE* pPipe = popen(cmd.c_str(), "r"))
{
char buf[128];
while (fgets(buf, 128, pPipe)) {
cmdOutput += std::string(buf, buf+128);
}
pclose(pPipe);
}
return cmdOutput;
}
std::string getSha1OfResponseData(const std::string& url)
{
const std::string pythonScript =
"import urllib.request as req; "
"import hashlib; "
"print(hashlib.sha1(req.urlopen('" + url + "').read()).hexdigest())";
const std::string cmd = "python3 -c \"" + pythonScript + "\"";
return runExternalCmdAndGetItsOutput(cmd);
}
TEST_F(ServerTest, CacheIdsOfStaticResourcesMatchTheSha1HashOfResourceContent)
{
for ( const Resource& res : all200Resources() ) {
if ( res.kind == STATIC_CONTENT ) {
const TestContext ctx{ {"url", res.url} };
const std::string fullUrl = "http://localhost:" + std::to_string(SERVER_PORT) + res.url;
const std::string sha1 = getSha1OfResponseData(fullUrl);
ASSERT_EQ(sha1.substr(0, 8), getCacheIdFromUrl(res.url)) << ctx;
}
}
}
const char* urls400[] = {
"/ROOT%23%3F/search",
"/ROOT%23%3F/search?content=zimfile",
@@ -764,89 +839,6 @@ TEST_F(ServerTest, Http404HtmlError)
</p>
)" },
{ /* url */ "/ROOT%23%3F/content/invalid-book/whatever",
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "url-not-found", "params" : { "url" : "/ROOT%23%3F/content/invalid-book/whatever" } } }, { "p" : { "msgid" : "suggest-search", "params" : { "PATTERN" : "whatever", "SEARCH_URL" : "/ROOT%23%3F/search?pattern=whatever" } } } ] })" &&
expected_body==R"(
<h1>Not Found</h1>
<p>
The requested URL "/ROOT%23%3F/content/invalid-book/whatever" was not found on this server.
</p>
<p>
Make a full text search for <a href="/ROOT%23%3F/search?pattern=whatever">whatever</a>
</p>
)" },
{ /* url */ "/ROOT%23%3F/content/zimfile/invalid-article",
book_name=="zimfile" &&
book_title=="Ray Charles" &&
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "url-not-found", "params" : { "url" : "/ROOT%23%3F/content/zimfile/invalid-article" } } }, { "p" : { "msgid" : "suggest-search", "params" : { "PATTERN" : "invalid-article", "SEARCH_URL" : "/ROOT%23%3F/search?content=zimfile&pattern=invalid-article" } } } ] })" &&
expected_body==R"(
<h1>Not Found</h1>
<p>
The requested URL "/ROOT%23%3F/content/zimfile/invalid-article" was not found on this server.
</p>
<p>
Make a full text search for <a href="/ROOT%23%3F/search?content=zimfile&pattern=invalid-article">invalid-article</a>
</p>
)" },
{ /* url */ R"(/ROOT%23%3F/content/"><svg onload=alert(1)>)",
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "url-not-found", "params" : { "url" : "/ROOT%23%3F/content/\"><svg onload%3Dalert(1)>" } } }, { "p" : { "msgid" : "suggest-search", "params" : { "PATTERN" : "\"><svg onload=alert(1)>", "SEARCH_URL" : "/ROOT%23%3F/search?pattern=%22%3E%3Csvg%20onload%3Dalert(1)%3E" } } } ] })" &&
expected_body==R"(
<h1>Not Found</h1>
<p>
The requested URL "/ROOT%23%3F/content/&quot;&gt;&lt;svg onload%3Dalert(1)&gt;" was not found on this server.
</p>
<p>
Make a full text search for <a href="/ROOT%23%3F/search?pattern=%22%3E%3Csvg%20onload%3Dalert(1)%3E">&quot;&gt;&lt;svg onload=alert(1)&gt;</a>
</p>
)" },
{ /* url */ R"(/ROOT%23%3F/content/zimfile/"><svg onload=alert(1)>)",
book_name=="zimfile" &&
book_title=="Ray Charles" &&
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "url-not-found", "params" : { "url" : "/ROOT%23%3F/content/zimfile/\"><svg onload%3Dalert(1)>" } } }, { "p" : { "msgid" : "suggest-search", "params" : { "PATTERN" : "\"><svg onload=alert(1)>", "SEARCH_URL" : "/ROOT%23%3F/search?content=zimfile&pattern=%22%3E%3Csvg%20onload%3Dalert(1)%3E" } } } ] })" &&
expected_body==R"(
<h1>Not Found</h1>
<p>
The requested URL "/ROOT%23%3F/content/zimfile/&quot;&gt;&lt;svg onload%3Dalert(1)&gt;" was not found on this server.
</p>
<p>
Make a full text search for <a href="/ROOT%23%3F/search?content=zimfile&pattern=%22%3E%3Csvg%20onload%3Dalert(1)%3E">&quot;&gt;&lt;svg onload=alert(1)&gt;</a>
</p>
)" },
// XXX: This test case is against a "</script>" string appearing inside
// XXX: javascript code that will confuse the HTML parser
{ /* url */ R"(/ROOT%23%3F/content/zimfile/</script>)",
book_name=="zimfile" &&
book_title=="Ray Charles" &&
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "url-not-found", "params" : { "url" : "/ROOT%23%3F/content/zimfile/</scr\ipt>" } } }, { "p" : { "msgid" : "suggest-search", "params" : { "PATTERN" : "script>", "SEARCH_URL" : "/ROOT%23%3F/search?content=zimfile&pattern=script%3E" } } } ] })" &&
expected_body==R"(
<h1>Not Found</h1>
<p>
The requested URL "/ROOT%23%3F/content/zimfile/&lt;/script&gt;" was not found on this server.
</p>
<p>
Make a full text search for <a href="/ROOT%23%3F/search?content=zimfile&pattern=script%3E">script&gt;</a>
</p>
)" },
{ /* url */ "/ROOT%23%3F/content/zimfile/invalid-article?userlang=test",
expected_page_title=="[I18N TESTING] Not Found - Try Again" &&
book_name=="zimfile" &&
book_title=="Ray Charles" &&
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "url-not-found", "params" : { "url" : "/ROOT%23%3F/content/zimfile/invalid-article" } } }, { "p" : { "msgid" : "suggest-search", "params" : { "PATTERN" : "invalid-article", "SEARCH_URL" : "/ROOT%23%3F/search?content=zimfile&pattern=invalid-article" } } } ] })" &&
expected_body==R"(
<h1>[I18N TESTING] Content not found, but at least the server is alive</h1>
<p>
[I18N TESTING] URL not found: /ROOT%23%3F/content/zimfile/invalid-article
</p>
<p>
[I18N TESTING] Make a full text search for <a href="/ROOT%23%3F/search?content=zimfile&pattern=invalid-article">invalid-article</a>
</p>
)" },
{ /* url */ "/ROOT%23%3F/raw/no-such-book/meta/Title",
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "url-not-found", "params" : { "url" : "/ROOT%23%3F/raw/no-such-book/meta/Title" } } }, { "p" : { "msgid" : "no-such-book", "params" : { "BOOK_NAME" : "no-such-book" } } } ] })" &&
expected_body==R"(
@@ -917,6 +909,131 @@ TEST_F(ServerTest, Http404HtmlError)
}
}
std::string htmlEscape(std::string s)
{
s = replace(s, "&", "&amp;");
s = replace(s, "<", "&lt;");
s = replace(s, ">", "&gt;");
s = replace(s, "\"", "&quot;");
return s;
}
std::string escapeJsString(std::string s)
{
s = replace(s, "</script>", "</scr\\ipt>");
s = replace(s, "\"", "\\\"");
return s;
}
std::string expectedSexy404ErrorHtml(const std::string& url)
{
const auto urlWithoutQuery = replace(url, "\\?.*$", "");
const auto htmlSafeUrl = htmlEscape(urlWithoutQuery);
const auto jsSafeUrl = escapeJsString(urlWithoutQuery);
const std::string englishText[] = {
"Page not found",
"Not found!",
"Oops. Page not found.",
"The requested path was not found:",
"The content you&apos;re looking for may still be available, but it might be located at a different place within the ZIM file.",
"Please:",
"Try using the search function to find the content you want",
"Look for keywords or titles related to the information you&apos;re seeking",
"This approach should help you locate the desired content, even if the original link isn&apos;t working properly."
};
const std::string translatedText[] = {
"Page [I18N] not [TESTING] found",
"[I18N] Not found! [TESTING]",
"[I18N TESTING] Oops. Larry Page could not be reached. He may be on paternity leave.",
"[I18N TESTING] The requested path was not found (in fact, nothing was found instead, either):",
"Sh*t happens. [I18N TESTING] Take it easy!",
"[I18N TESTING] Try one of the following:",
"[I18N TESTING] Check the spelling of the URL path",
"[I18N TESTING] Press the dice button",
"Good luck! [I18N TESTING]"
};
const bool shouldTranslate = url.find("userlang=test") != std::string::npos;
const std::string* const t = shouldTranslate ? translatedText : englishText;
return R"RAWSTRINGLITERAL(<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>)RAWSTRINGLITERAL" + t[0] + R"RAWSTRINGLITERAL(</title>
<link type="text/css" href="/ROOT%23%3F/skin/error.css?cacheid=b3fa90cf" rel="Stylesheet" />
<script>
window.KIWIX_RESPONSE_TEMPLATE = "&lt;!DOCTYPE html&gt;\n&lt;html&gt;\n &lt;head&gt;\n &lt;meta charset=&quot;utf-8&quot;&gt;\n &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no&quot; /&gt;\n &lt;title&gt;{{PAGE_TITLE}}&lt;/title&gt;\n &lt;link type=&quot;text/css&quot; href=&quot;{{root}}/skin/error.css?cacheid=b3fa90cf&quot; rel=&quot;Stylesheet&quot; /&gt;\n{{#KIWIX_RESPONSE_DATA}} &lt;script&gt;\n window.KIWIX_RESPONSE_TEMPLATE = &quot;{{KIWIX_RESPONSE_TEMPLATE}}&quot;;\n window.KIWIX_RESPONSE_DATA = {{{KIWIX_RESPONSE_DATA}}};\n &lt;/script&gt;{{/KIWIX_RESPONSE_DATA}}\n &lt;/head&gt;\n &lt;body&gt;\n &lt;header&gt;\n &lt;img src=&quot;{{root}}/skin/404.svg?cacheid=b6d648af&quot;\n alt=&quot;{{404_img_text}}&quot;\n aria-label=&quot;{{404_img_text}}&quot;\n title=&quot;{{404_img_text}}&quot;&gt;\n &lt;/header&gt;\n &lt;section class=&quot;intro&quot;&gt;\n &lt;h1&gt;{{PAGE_HEADING}}&lt;/h1&gt;\n &lt;p&gt;{{path_was_not_found_msg}}&lt;/p&gt;\n &lt;p&gt;&lt;code&gt;{{url_path}}&lt;/code&gt;&lt;/p&gt;\n &lt;/section&gt;\n &lt;section class=&quot;advice&quot;&gt;\n &lt;p&gt;{{advice.p1}}&lt;/p&gt;\n &lt;p class=&quot;list-intro&quot;&gt;{{advice.p2}}&lt;/p&gt;\n &lt;ul&gt;\n &lt;li&gt;{{advice.p3}}&lt;/li&gt;\n &lt;li&gt;{{advice.p4}}&lt;/li&gt;\n &lt;/ul&gt;\n &lt;p&gt;{{advice.p5}}&lt;/p&gt;\n &lt;/section&gt;\n &lt;/body&gt;\n&lt;/html&gt;\n";
window.KIWIX_RESPONSE_DATA = { "404_img_text" : { "msgid" : "404-img-text", "params" : { } }, "PAGE_HEADING" : { "msgid" : "new-404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "new-404-page-title", "params" : { } }, "advice" : { "p1" : { "msgid" : "404-advice.p1", "params" : { } }, "p2" : { "msgid" : "404-advice.p2", "params" : { } }, "p3" : { "msgid" : "404-advice.p3", "params" : { } }, "p4" : { "msgid" : "404-advice.p4", "params" : { } }, "p5" : { "msgid" : "404-advice.p5", "params" : { } } }, "path_was_not_found_msg" : { "msgid" : "path-was-not-found", "params" : { } }, "root" : "/ROOT%23%3F", "url_path" : ")RAWSTRINGLITERAL"
+ // inject the URL
jsSafeUrl // inject the URL
+ // inject the URL
R"RAWSTRINGLITERAL(" };
</script>
</head>
<body>
<header>
<img src="/ROOT%23%3F/skin/404.svg?cacheid=b6d648af"
alt=")RAWSTRINGLITERAL" + t[1] + R"RAWSTRINGLITERAL("
aria-label=")RAWSTRINGLITERAL" + t[1] + R"RAWSTRINGLITERAL("
title=")RAWSTRINGLITERAL" + t[1] + R"RAWSTRINGLITERAL(">
</header>
<section class="intro">
<h1>)RAWSTRINGLITERAL" + t[2] + R"RAWSTRINGLITERAL(</h1>
<p>)RAWSTRINGLITERAL" + t[3] + R"RAWSTRINGLITERAL(</p>
<p><code>)RAWSTRINGLITERAL"
+ // inject the URL
htmlSafeUrl // inject the URL
+ // inject the URL
R"RAWSTRINGLITERAL(</code></p>
</section>
<section class="advice">
<p>)RAWSTRINGLITERAL" + t[4] + R"RAWSTRINGLITERAL(</p>
<p class="list-intro">)RAWSTRINGLITERAL" + t[5] + R"RAWSTRINGLITERAL(</p>
<ul>
<li>)RAWSTRINGLITERAL" + t[6] + R"RAWSTRINGLITERAL(</li>
<li>)RAWSTRINGLITERAL" + t[7] + R"RAWSTRINGLITERAL(</li>
</ul>
<p>)RAWSTRINGLITERAL" + t[8] + R"RAWSTRINGLITERAL(</p>
</section>
</body>
</html>
)RAWSTRINGLITERAL";
}
TEST_F(ServerTest, HttpSexy404HtmlError)
{
using namespace TestingOfHtmlResponses;
const std::vector<std::string> testUrls {
// XXX: Nicer 404 error page no longer hints whether the error
// XXX: is because of the missing book/ZIM-file or a missing article
// XXX: inside a valid/existing book/ZIM-file. However it makes sense
// XXX: to preserve both cases.
"/ROOT%23%3F/content/invalid-book/whatever",
"/ROOT%23%3F/content/invalid-book/whatever?userlang=test",
"/ROOT%23%3F/content/zimfile/invalid-article",
"/ROOT%23%3F/content/zimfile/invalid-article?userlang=test",
// malicious URLs
R"(/ROOT%23%3F/content/"><svg onload=alert(1)>)",
R"(/ROOT%23%3F/content/zimfile/"><svg onload=alert(1)>)",
// XXX: This test case is against a "</script>" string appearing inside
// XXX: javascript code that will confuse the HTML parser
R"(/ROOT%23%3F/content/zimfile/</script>)",
};
for ( const auto& url : testUrls ) {
const TestContext ctx{ {"url", url} };
const auto r = zfs1_->GET(url.c_str());
EXPECT_EQ(r->status, 404) << ctx;
EXPECT_EQ(r->body, expectedSexy404ErrorHtml(url)) << ctx;
}
}
TEST_F(ServerTest, Http400HtmlError)
{
using namespace TestingOfHtmlResponses;
@@ -1132,12 +1249,12 @@ R"EXPECTEDRESPONSE(const uiLanguages = [
{
"iso_code": "ar",
"self_name": "الإنجليزية",
"translation_count": 25
"translation_count": 43
},
{
"iso_code": "bn",
"self_name": "বাংলা",
"translation_count": 14
"translation_count": 24
},
{
"iso_code": "br",
@@ -1157,7 +1274,7 @@ R"EXPECTEDRESPONSE(const uiLanguages = [
{
"iso_code": "de",
"self_name": "Deutsch",
"translation_count": 57
"translation_count": 67
},
{
"iso_code": "el",
@@ -1167,12 +1284,12 @@ R"EXPECTEDRESPONSE(const uiLanguages = [
{
"iso_code": "en",
"self_name": "English",
"translation_count": 58
"translation_count": 77
},
{
"iso_code": "es",
"self_name": "español",
"translation_count": 49
"translation_count": 67
},
{
"iso_code": "fi",
@@ -1182,7 +1299,7 @@ R"EXPECTEDRESPONSE(const uiLanguages = [
{
"iso_code": "fr",
"self_name": "Français",
"translation_count": 57
"translation_count": 68
},
{
"iso_code": "ha",
@@ -1192,22 +1309,27 @@ R"EXPECTEDRESPONSE(const uiLanguages = [
{
"iso_code": "he",
"self_name": "עברית",
"translation_count": 57
"translation_count": 69
},
{
"iso_code": "hi",
"self_name": "हिन्दी",
"translation_count": 49
"translation_count": 59
},
{
"iso_code": "hy",
"self_name": "Հայերեն",
"translation_count": 15
"translation_count": 25
},
{
"iso_code": "ia",
"self_name": "interlingua",
"translation_count": 57
"translation_count": 67
},
{
"iso_code": "id",
"self_name": "Bahasa Inggris",
"translation_count": 68
},
{
"iso_code": "ig",
@@ -1217,7 +1339,7 @@ R"EXPECTEDRESPONSE(const uiLanguages = [
{
"iso_code": "it",
"self_name": "italiano",
"translation_count": 38
"translation_count": 54
},
{
"iso_code": "ja",
@@ -1227,7 +1349,7 @@ R"EXPECTEDRESPONSE(const uiLanguages = [
{
"iso_code": "ko",
"self_name": "한국어",
"translation_count": 15
"translation_count": 64
},
{
"iso_code": "ku-latn",
@@ -1237,22 +1359,27 @@ R"EXPECTEDRESPONSE(const uiLanguages = [
{
"iso_code": "lb",
"self_name": "Lëtzebuergesch",
"translation_count": 22
"translation_count": 43
},
{
"iso_code": "mk",
"self_name": "македонски",
"translation_count": 57
"translation_count": 76
},
{
"iso_code": "ms",
"self_name": "Bahasa Melayu",
"translation_count": 14
},
{
"iso_code": "nb",
"self_name": "Engelsk",
"translation_count": 50
},
{
"iso_code": "nl",
"self_name": "Nederlands",
"translation_count": 49
"translation_count": 68
},
{
"iso_code": "nqo",
@@ -1267,17 +1394,27 @@ R"EXPECTEDRESPONSE(const uiLanguages = [
{
"iso_code": "pl",
"self_name": "Polski",
"translation_count": 31
"translation_count": 32
},
{
"iso_code": "pt-br",
"self_name": "Português",
"translation_count": 35
"translation_count": 65
},
{
"iso_code": "pt",
"self_name": "português",
"translation_count": 67
},
{
"iso_code": "ro",
"self_name": "Engleză",
"translation_count": 67
},
{
"iso_code": "ru",
"self_name": "русский",
"translation_count": 57
"translation_count": 67
},
{
"iso_code": "sc",
@@ -1292,7 +1429,7 @@ R"EXPECTEDRESPONSE(const uiLanguages = [
{
"iso_code": "skr-arab",
"self_name": "سرائیکی",
"translation_count": 20
"translation_count": 31
},
{
"iso_code": "sl",
@@ -1302,17 +1439,17 @@ R"EXPECTEDRESPONSE(const uiLanguages = [
{
"iso_code": "sq",
"self_name": "Shqip",
"translation_count": 49
"translation_count": 67
},
{
"iso_code": "sv",
"self_name": "Svenska",
"translation_count": 57
"translation_count": 67
},
{
"iso_code": "sw",
"self_name": "Kiswahili",
"translation_count": 57
"translation_count": 58
},
{
"iso_code": "te",
@@ -1327,12 +1464,12 @@ R"EXPECTEDRESPONSE(const uiLanguages = [
{
"iso_code": "zh-hans",
"self_name": "简体中文",
"translation_count": 57
"translation_count": 68
},
{
"iso_code": "zh-hant",
"self_name": "繁體中文",
"translation_count": 57
"translation_count": 76
}
])EXPECTEDRESPONSE");
}
@@ -1363,37 +1500,37 @@ TEST_F(ServerTest, UserLanguageControl)
"Default user language is English",
/*url*/ "/ROOT%23%3F/content/zimfile/invalid-article",
/*Accept-Language:*/ "",
/* expected <h1> */ "Not Found"
/* expected <h1> */ "Oops. Page not found."
},
{
"userlang URL query parameter is respected",
/*url*/ "/ROOT%23%3F/content/zimfile/invalid-article?userlang=en",
/*Accept-Language:*/ "",
/* expected <h1> */ "Not Found"
/* expected <h1> */ "Oops. Page not found."
},
{
"userlang URL query parameter is respected",
/*url*/ "/ROOT%23%3F/content/zimfile/invalid-article?userlang=test",
/*Accept-Language:*/ "",
/* expected <h1> */ "[I18N TESTING] Content not found, but at least the server is alive"
/* expected <h1> */ "[I18N TESTING] Oops. Larry Page could not be reached. He may be on paternity leave."
},
{
"'Accept-Language: *' is handled",
/*url*/ "/ROOT%23%3F/content/zimfile/invalid-article",
/*Accept-Language:*/ "*",
/* expected <h1> */ "Not Found"
/* expected <h1> */ "Oops. Page not found."
},
{
"Accept-Language: header is respected",
/*url*/ "/ROOT%23%3F/content/zimfile/invalid-article",
/*Accept-Language:*/ "test",
/* expected <h1> */ "[I18N TESTING] Content not found, but at least the server is alive"
/* expected <h1> */ "[I18N TESTING] Oops. Larry Page could not be reached. He may be on paternity leave."
},
{
"userlang query parameter takes precedence over Accept-Language",
/*url*/ "/ROOT%23%3F/content/zimfile/invalid-article?userlang=en",
/*Accept-Language:*/ "test",
/* expected <h1> */ "Not Found"
/* expected <h1> */ "Oops. Page not found."
},
{
"Most suitable language is selected from the Accept-Language header",
@@ -1401,7 +1538,7 @@ TEST_F(ServerTest, UserLanguageControl)
// with quality values) the most suitable language is selected.
/*url*/ "/ROOT%23%3F/content/zimfile/invalid-article",
/*Accept-Language:*/ "test;q=0.9, en;q=0.2",
/* expected <h1> */ "[I18N TESTING] Content not found, but at least the server is alive"
/* expected <h1> */ "[I18N TESTING] Oops. Larry Page could not be reached. He may be on paternity leave."
},
{
"Most suitable language is selected from the Accept-Language header",
@@ -1409,7 +1546,7 @@ TEST_F(ServerTest, UserLanguageControl)
// with quality values) the most suitable language is selected.
/*url*/ "/ROOT%23%3F/content/zimfile/invalid-article",
/*Accept-Language:*/ "test;q=0.2, en;q=0.9",
/* expected <h1> */ "Not Found"
/* expected <h1> */ "Oops. Page not found."
},
};

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