mirror of
https://github.com/kiwix/libkiwix.git
synced 2025-12-24 06:57:59 -05:00
Compare commits
411 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42a2ce2534 | ||
|
|
3945dda5d0 | ||
|
|
d65dd859da | ||
|
|
d94d2c1e8a | ||
|
|
a20b135f80 | ||
|
|
6d520a8aa7 | ||
|
|
f82bfc068f | ||
|
|
e6335be897 | ||
|
|
1074e833b7 | ||
|
|
9da5fbad1e | ||
|
|
1869fb4e8e | ||
|
|
536198fa38 | ||
|
|
ca808718f7 | ||
|
|
b65074f961 | ||
|
|
8b7d1ef9ec | ||
|
|
8b0f01fa9b | ||
|
|
33f22eb966 | ||
|
|
55c13c3d24 | ||
|
|
2b1f556c20 | ||
|
|
e0cd5a1642 | ||
|
|
0a9ba9b678 | ||
|
|
db9607e55e | ||
|
|
592e22732e | ||
|
|
17f0ad2cf4 | ||
|
|
4928509991 | ||
|
|
c2df0a99fe | ||
|
|
cffca3ad85 | ||
|
|
0a2bebe7a3 | ||
|
|
bdb1f09884 | ||
|
|
f98b79348b | ||
|
|
2b8927e66e | ||
|
|
d0fb8214c3 | ||
|
|
d5894092fd | ||
|
|
dd09e3ce5f | ||
|
|
92954bbbe4 | ||
|
|
7a9edccbc5 | ||
|
|
e9e76e0901 | ||
|
|
ad9377083f | ||
|
|
d857b0f8f6 | ||
|
|
759d430232 | ||
|
|
e402dcabcb | ||
|
|
54bd29e3ed | ||
|
|
5c8aa240ad | ||
|
|
39672f0532 | ||
|
|
e0491adc85 | ||
|
|
286649e8c3 | ||
|
|
b799c0648b | ||
|
|
050906c1b2 | ||
|
|
f5e35b4c5d | ||
|
|
2a858dcc82 | ||
|
|
ac9be80369 | ||
|
|
d0a48cc9cc | ||
|
|
67b7e25494 | ||
|
|
2b4b90f8a3 | ||
|
|
208dd96edd | ||
|
|
51ffa31037 | ||
|
|
968d1c1067 | ||
|
|
c205a4703b | ||
|
|
8bff8d5891 | ||
|
|
94e51e363c | ||
|
|
25e03ce597 | ||
|
|
8a3c4c92e0 | ||
|
|
95fc478e37 | ||
|
|
26253ebf8f | ||
|
|
34c6a3bfab | ||
|
|
5c1f8de891 | ||
|
|
334ca0295e | ||
|
|
e2186cfb7b | ||
|
|
a4985c62c7 | ||
|
|
2f9deb0eaa | ||
|
|
b8d975068e | ||
|
|
222327586e | ||
|
|
699cbccf38 | ||
|
|
fdc3a715c4 | ||
|
|
3bfcc5108f | ||
|
|
6225b4608a | ||
|
|
0bc9a25179 | ||
|
|
9433f7cef9 | ||
|
|
eba66a391f | ||
|
|
fa6c93950c | ||
|
|
850e330461 | ||
|
|
eccb8db7b7 | ||
|
|
33bb0141c0 | ||
|
|
b3b4064ad6 | ||
|
|
2fdd2066cd | ||
|
|
4dfcfbe1fa | ||
|
|
e912f0520e | ||
|
|
c4ced73f7c | ||
|
|
c244d95a94 | ||
|
|
04d301d024 | ||
|
|
e415958ae9 | ||
|
|
f9b8789723 | ||
|
|
fe806396f9 | ||
|
|
5729b6540c | ||
|
|
33c83eec4b | ||
|
|
be69584637 | ||
|
|
2ba29f76e1 | ||
|
|
222e4396c7 | ||
|
|
4c480952d1 | ||
|
|
79479788f9 | ||
|
|
3cd1f7854a | ||
|
|
58a211d01d | ||
|
|
07fc40da5a | ||
|
|
d961447e1e | ||
|
|
c9ebeb7b96 | ||
|
|
2d73ed31a9 | ||
|
|
6a0349e575 | ||
|
|
6d80edc04a | ||
|
|
b3a33747f0 | ||
|
|
f47490e1bc | ||
|
|
1ce909ae68 | ||
|
|
5eb31d7286 | ||
|
|
107421cdab | ||
|
|
bd474b9720 | ||
|
|
e66ba1a532 | ||
|
|
c1e58331d7 | ||
|
|
d776077c5f | ||
|
|
b8e997f805 | ||
|
|
b7421d7dae | ||
|
|
a0c99f879b | ||
|
|
c7e86c9dbb | ||
|
|
ad58a501b0 | ||
|
|
a55e8565d1 | ||
|
|
610b8cbb2a | ||
|
|
e087f1c82f | ||
|
|
e5d3e6ff07 | ||
|
|
d0aeac64d0 | ||
|
|
5da88c0ad7 | ||
|
|
c7d3a38a3e | ||
|
|
6b74395455 | ||
|
|
2eea6136d6 | ||
|
|
64eb0d10d6 | ||
|
|
664944f16c | ||
|
|
d34a0c5bf0 | ||
|
|
bb65d77229 | ||
|
|
98849831da | ||
|
|
2e3eae5615 | ||
|
|
93ace5cf45 | ||
|
|
cb777ed836 | ||
|
|
27e7840cce | ||
|
|
99c28b72b5 | ||
|
|
81b579cdcb | ||
|
|
f693f700bc | ||
|
|
50f04d7060 | ||
|
|
8fdaa5f4db | ||
|
|
297627fbc7 | ||
|
|
a3708c68ce | ||
|
|
a809c671fd | ||
|
|
31477bc99b | ||
|
|
6c37e2827e | ||
|
|
9138f91c31 | ||
|
|
9bd568fe0e | ||
|
|
eca7cf86e6 | ||
|
|
77f4fd7447 | ||
|
|
84ebee899c | ||
|
|
9eed5da3be | ||
|
|
20abebd623 | ||
|
|
58a1af85b9 | ||
|
|
585f55d885 | ||
|
|
8b00c2eb22 | ||
|
|
c8bddd6cf4 | ||
|
|
5d1b6274a8 | ||
|
|
0de9bd0a99 | ||
|
|
b62274efdd | ||
|
|
4a1498d8df | ||
|
|
f6df2342cf | ||
|
|
8bbda99cab | ||
|
|
95529d2c0a | ||
|
|
b80699916d | ||
|
|
534916929d | ||
|
|
02ab2ce5a5 | ||
|
|
8930095c52 | ||
|
|
bef3ec7694 | ||
|
|
9057686a25 | ||
|
|
723dd977fe | ||
|
|
0b87d4fe04 | ||
|
|
ea31e2f42f | ||
|
|
01bda6b2c0 | ||
|
|
de64a5a724 | ||
|
|
90dd1cb3f0 | ||
|
|
0b14fda94d | ||
|
|
fe965faf1b | ||
|
|
6ad1776242 | ||
|
|
cbfd3ec7c4 | ||
|
|
f6765137e7 | ||
|
|
c24e04c8da | ||
|
|
327fec1877 | ||
|
|
c8524b95bc | ||
|
|
0ac3130b0d | ||
|
|
425ae1efae | ||
|
|
920d603a89 | ||
|
|
f5c91cc272 | ||
|
|
3cdc036858 | ||
|
|
29bfaa5c5b | ||
|
|
bec80e8091 | ||
|
|
2da9801bac | ||
|
|
16ebc6611b | ||
|
|
d5a44b913e | ||
|
|
a63a162c58 | ||
|
|
c29cd8cf3b | ||
|
|
04bf1be9d6 | ||
|
|
59054aa5ad | ||
|
|
1b8dde0115 | ||
|
|
9d0f6a3170 | ||
|
|
c16ed0aa4c | ||
|
|
3d95b386c6 | ||
|
|
a3f5a654f2 | ||
|
|
801b1df304 | ||
|
|
2b8a071c6f | ||
|
|
00fae37f2d | ||
|
|
846404e959 | ||
|
|
fbcd160efd | ||
|
|
196185dd73 | ||
|
|
affb996769 | ||
|
|
418abbcefa | ||
|
|
00867a13f6 | ||
|
|
e096c7e2fd | ||
|
|
69341eab47 | ||
|
|
082727ebb6 | ||
|
|
75a4f8b806 | ||
|
|
2eaa1c4649 | ||
|
|
199a10d093 | ||
|
|
4812fb18f6 | ||
|
|
940818d801 | ||
|
|
5182a66b19 | ||
|
|
b688aa294a | ||
|
|
27ad77c566 | ||
|
|
7677f76854 | ||
|
|
513a8d1383 | ||
|
|
be464a5986 | ||
|
|
c2042c3be8 | ||
|
|
8d480c8b6d | ||
|
|
82ff88f5d8 | ||
|
|
2535f210b3 | ||
|
|
cb0a2c234a | ||
|
|
5a73a75798 | ||
|
|
0ea756c42a | ||
|
|
7108dfa9c2 | ||
|
|
9fd8e81de2 | ||
|
|
566b40a2f8 | ||
|
|
ff6d8a4b30 | ||
|
|
f456ce3e93 | ||
|
|
ece40966f1 | ||
|
|
65a777d4ed | ||
|
|
42295c9010 | ||
|
|
e8afcbe6ae | ||
|
|
c46cd403ae | ||
|
|
af96b19bd1 | ||
|
|
8a00e9383d | ||
|
|
964131ce47 | ||
|
|
97832c8436 | ||
|
|
beab8d7041 | ||
|
|
75bddbf725 | ||
|
|
5927550a36 | ||
|
|
135c6f875d | ||
|
|
83101679a0 | ||
|
|
ae4b652fb2 | ||
|
|
01b94418eb | ||
|
|
a1ce3d10b1 | ||
|
|
b7eadf95bf | ||
|
|
a6cf161341 | ||
|
|
618a718645 | ||
|
|
c2cc4c39f1 | ||
|
|
8477e04ffa | ||
|
|
5345d43017 | ||
|
|
843adb3397 | ||
|
|
4fe4a88574 | ||
|
|
6ee09114eb | ||
|
|
7366938785 | ||
|
|
84405b1318 | ||
|
|
a0c4118fd3 | ||
|
|
72147aec5b | ||
|
|
016072292c | ||
|
|
2964cc5e92 | ||
|
|
8d766335b4 | ||
|
|
5450bcd3c2 | ||
|
|
a0b66eae0c | ||
|
|
22c75245a5 | ||
|
|
f40c3426a5 | ||
|
|
8e6569362c | ||
|
|
eb328ed73d | ||
|
|
21e3c5c19f | ||
|
|
f0927fec49 | ||
|
|
66693cd73e | ||
|
|
be8a60c330 | ||
|
|
8009edd349 | ||
|
|
3733e506c1 | ||
|
|
9fe81e9bce | ||
|
|
4ab6215046 | ||
|
|
ff88430227 | ||
|
|
922c138809 | ||
|
|
fa9ebf55fc | ||
|
|
bc9b5a0354 | ||
|
|
719e947ddf | ||
|
|
e3fffd9b23 | ||
|
|
6ef4f6396e | ||
|
|
d8b4c1584c | ||
|
|
1fc006f639 | ||
|
|
a8368b3a0d | ||
|
|
068555de38 | ||
|
|
0168764f4c | ||
|
|
181893d31a | ||
|
|
5b9daf0d9d | ||
|
|
4e64d26ede | ||
|
|
5e669cd65c | ||
|
|
2749564424 | ||
|
|
ddde6db16f | ||
|
|
50d1394a0a | ||
|
|
a6040b2ecd | ||
|
|
4e755bc949 | ||
|
|
cfab4c946a | ||
|
|
57a265f73c | ||
|
|
3f945813f2 | ||
|
|
86100b39ed | ||
|
|
b2ae6d1fca | ||
|
|
e82b62c552 | ||
|
|
5fba3f434e | ||
|
|
3ac36e8ebd | ||
|
|
1babbc0e4a | ||
|
|
6b05eeb24b | ||
|
|
73b855ce6b | ||
|
|
eaca7010bc | ||
|
|
6efdc43964 | ||
|
|
7a0ab3a429 | ||
|
|
3e9d50fecb | ||
|
|
f3a604380c | ||
|
|
167e0dc4b3 | ||
|
|
14c9530afa | ||
|
|
8d97686b81 | ||
|
|
b16f6b9561 | ||
|
|
a546effa15 | ||
|
|
699f96ca0d | ||
|
|
5a0644d32b | ||
|
|
903f476f77 | ||
|
|
bf1ab03332 | ||
|
|
82cb1133e5 | ||
|
|
9b9c61a194 | ||
|
|
c768d05b5b | ||
|
|
fe018efc70 | ||
|
|
e625c25ef1 | ||
|
|
b2ae1d66f5 | ||
|
|
2818dd3151 | ||
|
|
09eec822c1 | ||
|
|
34cd553642 | ||
|
|
70dd738801 | ||
|
|
958067d94d | ||
|
|
33a3277400 | ||
|
|
8f5714be07 | ||
|
|
c4fa42f20b | ||
|
|
795fcb9de4 | ||
|
|
c697611064 | ||
|
|
e5dab19844 | ||
|
|
1f44465d09 | ||
|
|
258a6d029f | ||
|
|
fc211d9a2e | ||
|
|
aff801e6cc | ||
|
|
3479589d53 | ||
|
|
d2f20dba66 | ||
|
|
dc3960c5f8 | ||
|
|
1f9026f295 | ||
|
|
30b3f05497 | ||
|
|
13a6863183 | ||
|
|
bb1a730253 | ||
|
|
e1f067c086 | ||
|
|
103a4516db | ||
|
|
bceba4da06 | ||
|
|
e14de69271 | ||
|
|
d2fedf9123 | ||
|
|
b151a2a480 | ||
|
|
8b8a2eede7 | ||
|
|
f3d3ab13cb | ||
|
|
1553d52593 | ||
|
|
f298acd45f | ||
|
|
0b542fe66d | ||
|
|
e72fc2391d | ||
|
|
d39e91f6bc | ||
|
|
0b7cd614c6 | ||
|
|
54191bcfab | ||
|
|
797f4c432c | ||
|
|
c57b8a0c7c | ||
|
|
aee6c23082 | ||
|
|
af228bf45f | ||
|
|
b9323f17bb | ||
|
|
8993f99587 | ||
|
|
96b6f41244 | ||
|
|
3f0ea083e6 | ||
|
|
9c5f5c7be0 | ||
|
|
9375f97b60 | ||
|
|
2ad5e510c6 | ||
|
|
a2e56e2422 | ||
|
|
8cc724b4a4 | ||
|
|
fa212fd6ae | ||
|
|
c0073b3bc7 | ||
|
|
0d2b6b3344 | ||
|
|
5f27b4b651 | ||
|
|
7a85c92025 | ||
|
|
6e2be481fd | ||
|
|
db3b76247f | ||
|
|
6a651e04e5 | ||
|
|
22ea3106c5 | ||
|
|
2d132d701e | ||
|
|
f81a5a1a4b | ||
|
|
3dce025f47 | ||
|
|
e470c97f74 | ||
|
|
a7ea908bcd | ||
|
|
41f25083da | ||
|
|
3188b0afe6 | ||
|
|
f8aae395f3 | ||
|
|
c5088aad7b | ||
|
|
269a659160 | ||
|
|
7161df9e4c |
189
.github/workflows/ci.yml
vendored
189
.github/workflows/ci.yml
vendored
@@ -8,126 +8,193 @@ on:
|
||||
|
||||
jobs:
|
||||
macOS:
|
||||
runs-on: macos-13
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
target:
|
||||
- macos-aarch64-dyn
|
||||
- macos-x86_64-dyn
|
||||
- ios-arm64-dyn
|
||||
- ios-x86_64-dyn
|
||||
include:
|
||||
- target: macos-aarch64-dyn
|
||||
arch_name: arm64-apple-macos
|
||||
run_test: true
|
||||
- target: macos-x86_64-dyn
|
||||
arch_name: x86_64-apple-darwin
|
||||
run_test: true
|
||||
- target: ios-arm64-dyn
|
||||
arch_name: aarch64-apple-ios
|
||||
run_test: false
|
||||
- target: ios-x86_64-dyn
|
||||
arch_name: x86-apple-ios-simulator
|
||||
run_test: false
|
||||
runs-on: macos-15
|
||||
|
||||
env:
|
||||
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.11.2_1 to python@3.11.3 fails to overwrite those
|
||||
rm -f /usr/local/bin/2to3 /usr/local/bin/2to3-3.11 /usr/local/bin/idle3 /usr/local/bin/idle3.11 /usr/local/bin/pydoc3 /usr/local/bin/pydoc3.11 /usr/local/bin/python3 /usr/local/bin/python3-config /usr/local/bin/python3.11 /usr/local/bin/python3.11-config
|
||||
brew install pkg-config ninja meson
|
||||
brew install ninja meson
|
||||
|
||||
- name: Install dependencies
|
||||
env:
|
||||
ARCHIVE_NAME: deps2_macos_native_dyn_libkiwix.tar.xz
|
||||
run: |
|
||||
wget -O- https://tmp.kiwix.org/ci/${{env.ARCHIVE_NAME}} | tar -xJ -C ${{env.HOME}}
|
||||
uses: kiwix/kiwix-build/actions/dl_deps_archive@main
|
||||
with:
|
||||
target_platform: ${{ matrix.target }}
|
||||
|
||||
- name: Compile source code
|
||||
- name: Compile
|
||||
env:
|
||||
PKG_CONFIG_PATH: ${{env.HOME}}/BUILD_native_dyn/INSTALL/lib/pkgconfig
|
||||
CPPFLAGS: -I${{env.HOME}}/BUILD_native_dyn/INSTALL/include
|
||||
PKG_CONFIG_PATH: ${{env.HOME}}/BUILD_${{matrix.arch_name}}/INSTALL/lib/pkgconfig
|
||||
CPPFLAGS: -I${{env.HOME}}/BUILD_${{matrix.arch_name}}/INSTALL/include
|
||||
MESON_OPTION: --default-library=shared -Db_coverage=true
|
||||
MESON_CROSSFILE: ${{env.HOME}}/BUILD_${{matrix.arch_name}}/meson_cross_file.txt
|
||||
shell: bash
|
||||
run: |
|
||||
meson . build --default-library=shared -Db_coverage=true
|
||||
if [ -e $MESON_CROSSFILE ]; then
|
||||
MESON_OPTION="$MESON_OPTION --cross-file $MESON_CROSSFILE -Dstatic-linkage=true"
|
||||
fi
|
||||
meson . build ${MESON_OPTION}
|
||||
ninja -C build
|
||||
|
||||
- name: Test libkiwix
|
||||
if: matrix.run_test
|
||||
env:
|
||||
SKIP_BIG_MEMORY_TEST: 1
|
||||
LD_LIBRARY_PATH: ${{env.HOME}}/BUILD_native_dyn/INSTALL/lib:${{env.HOME}}/BUILD_native_dyn/INSTALL/lib64
|
||||
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
|
||||
matrix:
|
||||
name:
|
||||
- native_static
|
||||
- native_dyn
|
||||
- android_arm
|
||||
- android_arm64
|
||||
- win32_static
|
||||
- win32_dyn
|
||||
target:
|
||||
- linux-x86_64-static
|
||||
- linux-x86_64-dyn
|
||||
- android-arm
|
||||
- android-arm64
|
||||
image_variant: ['jammy']
|
||||
include:
|
||||
- name: native_static
|
||||
target: native_static
|
||||
image_variant: focal
|
||||
- target: linux-x86_64-static
|
||||
lib_postfix: '/x86_64-linux-gnu'
|
||||
- name: native_dyn
|
||||
target: native_dyn
|
||||
image_variant: focal
|
||||
arch_name: linux-x86_64
|
||||
run_test: true
|
||||
coverage: true
|
||||
- target: linux-x86_64-dyn
|
||||
lib_postfix: '/x86_64-linux-gnu'
|
||||
- name: android_arm
|
||||
target: android_arm
|
||||
image_variant: focal
|
||||
arch_name: linux-x86_64
|
||||
run_test: true
|
||||
coverage: true
|
||||
- target: android-arm
|
||||
lib_postfix: '/arm-linux-androideabi'
|
||||
- name: android_arm64
|
||||
target: android_arm64
|
||||
image_variant: focal
|
||||
arch_name: arm-linux-androideabi
|
||||
run_test: false
|
||||
coverage: false
|
||||
- target: android-arm64
|
||||
lib_postfix: '/aarch64-linux-android'
|
||||
- name: win32_static
|
||||
target: win32_static
|
||||
image_variant: f35
|
||||
lib_postfix: '64'
|
||||
- name: win32_dyn
|
||||
target: win32_dyn
|
||||
image_variant: f35
|
||||
lib_postfix: '64'
|
||||
arch_name: aarch64-linux-android
|
||||
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"
|
||||
image: "ghcr.io/kiwix/kiwix-build_ci_${{matrix.image_variant}}:2025-06-07"
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- name: Install deps
|
||||
shell: bash
|
||||
run: |
|
||||
ARCHIVE_NAME=deps2_${OS_NAME}_${{matrix.target}}_libkiwix.tar.xz
|
||||
wget -O- http://tmp.kiwix.org/ci/${ARCHIVE_NAME} | tar -xJ -C /home/runner
|
||||
uses: actions/checkout@v4
|
||||
- name: Install dependencies
|
||||
uses: kiwix/kiwix-build/actions/dl_deps_archive@main
|
||||
with:
|
||||
target_platform: ${{ matrix.target }}
|
||||
- name: Compile
|
||||
shell: bash
|
||||
run: |
|
||||
meson --version
|
||||
if [[ "${{matrix.target}}" =~ .*_dyn ]]; then
|
||||
if [[ "${{matrix.target}}" =~ .*-dyn ]]; then
|
||||
MESON_OPTION="--default-library=shared"
|
||||
else
|
||||
MESON_OPTION="--default-library=static"
|
||||
fi
|
||||
if [[ "${{matrix.target}}" =~ native_.* ]]; then
|
||||
MESON_OPTION="$MESON_OPTION -Db_coverage=true"
|
||||
if [ -e "${{env.HOME}}/BUILD_${{matrix.arch_name}}/meson_cross_file.txt" ]; then
|
||||
MESON_OPTION="$MESON_OPTION --cross-file ${{env.HOME}}/BUILD_${{matrix.arch_name}}/meson_cross_file.txt"
|
||||
else
|
||||
MESON_OPTION="$MESON_OPTION --cross-file $HOME/BUILD_${{matrix.target}}/meson_cross_file.txt"
|
||||
MESON_OPTION="$MESON_OPTION -Db_coverage=true"
|
||||
fi
|
||||
if [[ "${{matrix.target}}" =~ android_.* ]]; then
|
||||
if [[ "${{matrix.target}}" =~ android-.* ]]; then
|
||||
MESON_OPTION="$MESON_OPTION -Dstatic-linkage=true"
|
||||
fi
|
||||
meson . build ${MESON_OPTION}
|
||||
cd build
|
||||
ninja
|
||||
env:
|
||||
PKG_CONFIG_PATH: "/home/runner/BUILD_${{matrix.target}}/INSTALL/lib/pkgconfig:/home/runner/BUILD_${{matrix.target}}/INSTALL/lib${{matrix.lib_postfix}}/pkgconfig"
|
||||
CPPFLAGS: "-I/home/runner/BUILD_${{matrix.target}}/INSTALL/include"
|
||||
PKG_CONFIG_PATH: "/home/runner/BUILD_${{matrix.arch_name}}/INSTALL/lib/pkgconfig:/home/runner/BUILD_${{matrix.arch_name}}/INSTALL/lib${{matrix.lib_postfix}}/pkgconfig"
|
||||
CPPFLAGS: "-I/home/runner/BUILD_${{matrix.arch_name}}/INSTALL/include"
|
||||
- name: Test
|
||||
if: startsWith(matrix.target, 'native_')
|
||||
if: matrix.run_test
|
||||
shell: bash
|
||||
run: |
|
||||
cd build
|
||||
meson test --verbose
|
||||
ninja coverage
|
||||
if [[ "${{matrix.coverage}}" = "true" ]]; then
|
||||
ninja coverage
|
||||
fi
|
||||
env:
|
||||
LD_LIBRARY_PATH: "/home/runner/BUILD_${{matrix.target}}/INSTALL/lib:/home/runner/BUILD_${{matrix.target}}/INSTALL/lib${{matrix.lib_postfix}}"
|
||||
LD_LIBRARY_PATH: "/home/runner/BUILD_${{matrix.arch_name}}/INSTALL/lib:/home/runner/BUILD_${{matrix.arch_name}}/INSTALL/lib${{matrix.lib_postfix}}"
|
||||
SKIP_BIG_MEMORY_TEST: 1
|
||||
|
||||
- name: Publish coverage
|
||||
if: startsWith(matrix.target, 'native_')
|
||||
if: matrix.coverage
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
67
.github/workflows/package.yml
vendored
67
.github/workflows/package.yml
vendored
@@ -15,11 +15,15 @@ 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 +31,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'
|
||||
@@ -55,20 +88,12 @@ jobs:
|
||||
args: --no-sign
|
||||
ppa: ${{ steps.ppa.outputs.ppa }}
|
||||
|
||||
- uses: legoktm/gh-action-build-deb@ubuntu-focal
|
||||
if: matrix.distro == 'ubuntu-focal'
|
||||
name: Build package for ubuntu-focal
|
||||
id: build-ubuntu-focal
|
||||
with:
|
||||
args: --no-sign
|
||||
ppa: ${{ steps.ppa.outputs.ppa }}
|
||||
|
||||
- uses: actions/upload-artifact@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 +102,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:
|
||||
|
||||
70
ChangeLog
70
ChangeLog
@@ -1,3 +1,70 @@
|
||||
libkiwix 14.1.1
|
||||
===============
|
||||
|
||||
* Server:
|
||||
- Fix regression for kiwix-serve --nosearchbar (@veloman-yunkan #1250)
|
||||
- Avoid results content interpretation... crash in fulltext search (@vighnesh-sawant #1241)
|
||||
- Fix for intermittent /content/blank.html errors (@veloman-yunkan #1249)
|
||||
|
||||
libkiwix 14.1.0
|
||||
===============
|
||||
|
||||
* Server:
|
||||
- Viewer detects & tracks intrapage navigation anchors too (@veloman-yunkan #1213)
|
||||
- Add support for catalog only mode (@veloman-yunkan #1219)
|
||||
- Add API which returns server access url (@vighnesh-sawant #1234)
|
||||
- Fix chrome searchbar placeholder text overflow (@aditii2712 #1185)
|
||||
- Fix magnet link queryStyring (@rgaudin #1160)
|
||||
- Improve chrome printing stylesheet (@kelson42 #1202)
|
||||
- Default white background (@kelson42 #1205)
|
||||
|
||||
* Other:
|
||||
- Switched to the new libzim illustrations API (@veloman-yunkan #1226)
|
||||
- Stop building Windows with DEBUG symbols in CI (@kelson42 #1165)
|
||||
- Update many things in the CI/CD (@kelson42 #1203 #1194 #1209 #1207 #1235)
|
||||
- Requires now libzim 9.4.0 (@kelson42 #1231)
|
||||
- Fix compilation for FreeBSD (@OICe2 #1173 #1174)
|
||||
- Wait up to 1s to let aria2c to start before complaining (@kelson42 #1169)
|
||||
|
||||
libkiwix 14.0.0
|
||||
===============
|
||||
|
||||
* Server:
|
||||
- Support of IPv6 (@veloman-yunkan @aryanA101a #1074 #1093)
|
||||
- Better public IP configuration/detection (@sgourdas #1132)
|
||||
- Fix API errors in catalog searches if Xapian keyword in used (@veloman-yunkan #1137)
|
||||
- Clearly define which Web browsers are supported (@kelson42 @rgaudin @jaifroid @benoit74 #1132)
|
||||
- Improve welcome page download buttons (@veloman-yunkan #1094)
|
||||
- Better handling of external (non-HTTP) links (@veloman-yunkan #1123)
|
||||
- Fix book illustration size on welcome page to 48x48 pixels (@veloman-yunkan #1127)
|
||||
- Remove "Multiple Languages" in language filter (@veloman-yunkan #1098)
|
||||
- Stop transforming tags casing (@kelson42 @veloman-yunkan #1079 #1121)
|
||||
- ZIM file size consistently advertised in MiB (@harsha-mangena #1132)
|
||||
- Few new supported languages in the filter (@kelson42 #1080)
|
||||
- Improve accesskeys (@kelson42 #1075)
|
||||
- Add OpenSearch <link> to head of pages (@kelson42 #1070)
|
||||
* Compilation/Packaging:
|
||||
- Multiple fixes around deb packaging (@kelson42 #1108 #1114 #1135)
|
||||
- Generating of libkiwix.pc via Meson (@veloman-yunkan #1133)
|
||||
- Native Windows CI/CD (@mgautierfr @kelson42 #1113 #1125)
|
||||
- Better check (maximum) libzim version (@kelson42 #1124)
|
||||
- Multiple automated tests improvements (@veloman-yunkan #1068 #1067)
|
||||
* Other:
|
||||
- Deleted supported env. variable `$KIWIX_DATA_DIR` and `kiwix::getDataDirectory()` (@sgourdas #1107)
|
||||
- New string slugification for filenames (@shaopenglin #1105)
|
||||
- Multiple improvements around aria2c download mgmt. (@veloman-yunkan #1097)
|
||||
|
||||
libkiwix 13.1.0
|
||||
===============
|
||||
|
||||
* Server:
|
||||
- Properly translated error pages (@veloman-yunkan #1032)
|
||||
- Properly translated search result page (@veloman-yunkan #1046)
|
||||
- Default UI language is resolved in frontend (@veloman-yunkan #1044)
|
||||
- Better support of older Web browsers by polyfilling replaceAll() (@veloman-yunkan #1054)
|
||||
* New API to migrate bookmarks between books (@mgautierfr #1043)
|
||||
* Fixed compilation on Haiku OS (@Begasus #1048)
|
||||
|
||||
libkiwix 13.0.0
|
||||
===============
|
||||
|
||||
@@ -53,8 +120,6 @@ libkiwix 12.1.0
|
||||
* Remove libkiwix android publisher from the repository (@kelson42 #884)
|
||||
* Various fixes of meson and CI. (@mgautierfr @kelson42)
|
||||
|
||||
|
||||
|
||||
libkiwix 12.0.0
|
||||
===============
|
||||
|
||||
@@ -94,7 +159,6 @@ libkiwix 12.0.0
|
||||
* Fix documentation (@kelson42 #816)
|
||||
* Udpate translation (#787 #839 #847)
|
||||
|
||||
|
||||
libkiwix 11.0.0
|
||||
===============
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ GNU/Linux, macOS, Android, iOS, ...).
|
||||
|
||||
[](https://download.kiwix.org/release/libkiwix/)
|
||||
[](https://github.com/kiwix/libkiwix/wiki/Repology)
|
||||
[](https://github.com/kiwix/libkiwix/actions?query=branch%3Amain)
|
||||
[](https://github.com/kiwix/libkiwix/actions?query=branch%3Amain)
|
||||
[](https://libkiwix.readthedocs.org/en/latest/?badge=latest)
|
||||
[](https://www.codefactor.io/repository/github/kiwix/libkiwix)
|
||||
[](https://codecov.io/gh/kiwix/libkiwix)
|
||||
|
||||
17
debian/control
vendored
17
debian/control
vendored
@@ -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
|
||||
|
||||
@@ -29,19 +29,33 @@ class xml_node;
|
||||
namespace kiwix
|
||||
{
|
||||
|
||||
class Book;
|
||||
/**
|
||||
* A class to store information about a bookmark (an article in a book)
|
||||
*/
|
||||
class Bookmark
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* Create an empty bookmark.
|
||||
*
|
||||
* Bookmark must be populated with `set*` methods
|
||||
*/
|
||||
Bookmark();
|
||||
|
||||
/**
|
||||
* Create a bookmark given a Book, a path and a title.
|
||||
*/
|
||||
Bookmark(const Book& book, const std::string& path, const std::string& title);
|
||||
|
||||
~Bookmark();
|
||||
|
||||
void updateFromXml(const pugi::xml_node& node);
|
||||
|
||||
const std::string& getBookId() const { return m_bookId; }
|
||||
const std::string& getBookTitle() const { return m_bookTitle; }
|
||||
const std::string& getBookName() const { return m_bookName; }
|
||||
const std::string& getBookFlavour() const { return m_bookFlavour; }
|
||||
const std::string& getUrl() const { return m_url; }
|
||||
const std::string& getTitle() const { return m_title; }
|
||||
const std::string& getLanguage() const { return m_language; }
|
||||
@@ -49,6 +63,8 @@ class Bookmark
|
||||
|
||||
void setBookId(const std::string& bookId) { m_bookId = bookId; }
|
||||
void setBookTitle(const std::string& bookTitle) { m_bookTitle = bookTitle; }
|
||||
void setBookName(const std::string& bookName) { m_bookName = bookName; }
|
||||
void setBookFlavour(const std::string& bookFlavour) { m_bookFlavour = bookFlavour; }
|
||||
void setUrl(const std::string& url) { m_url = url; }
|
||||
void setTitle(const std::string& title) { m_title = title; }
|
||||
void setLanguage(const std::string& language) { m_language = language; }
|
||||
@@ -57,6 +73,8 @@ class Bookmark
|
||||
protected:
|
||||
std::string m_bookId;
|
||||
std::string m_bookTitle;
|
||||
std::string m_bookName;
|
||||
std::string m_bookFlavour;
|
||||
std::string m_url;
|
||||
std::string m_title;
|
||||
std::string m_language;
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
namespace kiwix {
|
||||
|
||||
enum class IpMode { IPV4, IPV6, ALL, AUTO }; // AUTO: Server decides the protocol
|
||||
typedef zim::size_type size_type;
|
||||
typedef zim::offset_type offset_type;
|
||||
|
||||
|
||||
@@ -168,8 +168,16 @@ class Download {
|
||||
*/
|
||||
class Downloader
|
||||
{
|
||||
public:
|
||||
Downloader();
|
||||
public: // types
|
||||
typedef std::vector<std::pair<std::string, std::string>> Options;
|
||||
|
||||
public: // functions
|
||||
/*
|
||||
* Create a new Downloader object.
|
||||
*
|
||||
* @param sessionFileDir: The directory where aria2 will store its session file.
|
||||
*/
|
||||
explicit Downloader(std::string sessionFileDir);
|
||||
virtual ~Downloader();
|
||||
|
||||
void close();
|
||||
@@ -177,14 +185,22 @@ class Downloader
|
||||
/**
|
||||
* Start a new download.
|
||||
*
|
||||
* This method is thread safe and return a pointer to a newly created `Download`.
|
||||
* This method is thread safe and returns a pointer to a newly created
|
||||
* `Download` or an existing one with a matching URI. In the latter case
|
||||
* the options parameter is ignored, which can lead to surprising results.
|
||||
* For example, if the old and new download requests (sharing the same URI)
|
||||
* have different values for the download directory or output file name
|
||||
* options, after the download is reported to be complete the downloaded file
|
||||
* will be present only at the location specified for the first request.
|
||||
*
|
||||
* User should call `update` on the returned `Download` to have an accurate status.
|
||||
*
|
||||
* @param uri: The uri of the thing to download.
|
||||
* @param downloadDir: The download directory where the thing should be stored (takes precedence over any "dir" in `options`).
|
||||
* @param options: A series of pair <option_name, option_value> to pass to aria.
|
||||
* @return: The newly created Download.
|
||||
*/
|
||||
std::shared_ptr<Download> startDownload(const std::string& uri, const std::vector<std::pair<std::string, std::string>>& 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)
|
||||
@@ -206,7 +222,7 @@ class Downloader
|
||||
*/
|
||||
std::vector<std::string> getDownloadIds() const;
|
||||
|
||||
private:
|
||||
private: // data
|
||||
mutable std::mutex m_lock;
|
||||
std::map<std::string, std::shared_ptr<Download>> m_knownDownloads;
|
||||
std::shared_ptr<Aria2> mp_aria;
|
||||
|
||||
92
include/i18n.h
Normal file
92
include/i18n.h
Normal file
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright 2024 Veloman Yunkan <veloman.yunkan@gmail.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||
* MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef KIWIX_I18N
|
||||
#define KIWIX_I18N
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
namespace kiwix
|
||||
{
|
||||
|
||||
std::string getTranslatedString(const std::string& lang, const std::string& key);
|
||||
|
||||
namespace i18n
|
||||
{
|
||||
|
||||
typedef std::map<std::string, std::string> Parameters;
|
||||
|
||||
std::string expandParameterizedString(const std::string& lang,
|
||||
const std::string& key,
|
||||
const Parameters& params);
|
||||
|
||||
class GetTranslatedString
|
||||
{
|
||||
public:
|
||||
explicit GetTranslatedString(const std::string& lang) : m_lang(lang) {}
|
||||
|
||||
std::string operator()(const std::string& key) const
|
||||
{
|
||||
return getTranslatedString(m_lang, key);
|
||||
}
|
||||
|
||||
std::string operator()(const std::string& key, const Parameters& params) const
|
||||
{
|
||||
return expandParameterizedString(m_lang, key, params);
|
||||
}
|
||||
|
||||
private:
|
||||
const std::string m_lang;
|
||||
};
|
||||
|
||||
} // namespace i18n
|
||||
|
||||
class ParameterizedMessage
|
||||
{
|
||||
public: // types
|
||||
typedef i18n::Parameters Parameters;
|
||||
|
||||
public: // functions
|
||||
ParameterizedMessage(const std::string& msgId, const Parameters& params)
|
||||
: msgId(msgId)
|
||||
, params(params)
|
||||
{}
|
||||
|
||||
std::string getText(const std::string& lang) const;
|
||||
|
||||
const std::string& getMsgId() const { return msgId; }
|
||||
const Parameters& getParams() const { return params; }
|
||||
|
||||
private: // data
|
||||
const std::string msgId;
|
||||
const Parameters params;
|
||||
};
|
||||
|
||||
inline ParameterizedMessage nonParameterizedMessage(const std::string& msgId)
|
||||
{
|
||||
const ParameterizedMessage::Parameters noParams;
|
||||
return ParameterizedMessage(msgId, noParams);
|
||||
}
|
||||
|
||||
std::string translateBookCategory(const std::string& lang, const std::string& category);
|
||||
|
||||
} // namespace kiwix
|
||||
|
||||
#endif // KIWIX_I18N
|
||||
@@ -55,6 +55,22 @@ enum supportedListMode {
|
||||
NOVALID = 1 << 5
|
||||
};
|
||||
|
||||
enum MigrationMode {
|
||||
/** When migrating bookmarks, do not allow to migrate to an older book than the currently pointed one
|
||||
* (or date stored in the bookmark if book is invalid)
|
||||
*
|
||||
* If no newer books are found, no upgrade is made.
|
||||
*/
|
||||
UPGRADE_ONLY = 0,
|
||||
|
||||
/** Try hard to do a migration. This mostly does:
|
||||
* - Try to find a newer book.
|
||||
* - If book is invalid: find a best book, potentially older.
|
||||
* Older book will never be returned if current book is a valid one.
|
||||
*/
|
||||
ALLOW_DOWNGRADE = 1,
|
||||
};
|
||||
|
||||
class Filter {
|
||||
public: // types
|
||||
using Tags = std::vector<std::string>;
|
||||
@@ -71,6 +87,7 @@ class Filter {
|
||||
std::string _query;
|
||||
bool _queryIsPartial;
|
||||
std::string _name;
|
||||
std::string _flavour;
|
||||
|
||||
public: // functions
|
||||
Filter();
|
||||
@@ -130,6 +147,7 @@ class Filter {
|
||||
Filter& maxSize(size_t size);
|
||||
Filter& query(std::string query, bool partial=true);
|
||||
Filter& name(std::string name);
|
||||
Filter& flavour(std::string flavour);
|
||||
Filter& clearLang();
|
||||
Filter& clearCategory();
|
||||
|
||||
@@ -152,6 +170,9 @@ class Filter {
|
||||
bool hasCreator() const;
|
||||
const std::string& getCreator() const { return _creator; }
|
||||
|
||||
bool hasFlavour() const;
|
||||
const std::string& getFlavour() const { return _flavour; }
|
||||
|
||||
const Tags& getAcceptTags() const { return _acceptTags; }
|
||||
const Tags& getRejectTags() const { return _rejectTags; }
|
||||
|
||||
@@ -250,7 +271,7 @@ class Library: public std::enable_shared_from_this<Library>
|
||||
void addBookmark(const Bookmark& bookmark);
|
||||
|
||||
/**
|
||||
* Remove a bookmarkk
|
||||
* Remove a bookmark
|
||||
*
|
||||
* @param zimId The zimId of the bookmark.
|
||||
* @param url The url of the bookmark.
|
||||
@@ -258,6 +279,66 @@ class Library: public std::enable_shared_from_this<Library>
|
||||
*/
|
||||
bool removeBookmark(const std::string& zimId, const std::string& url);
|
||||
|
||||
/**
|
||||
* Migrate all invalid bookmarks.
|
||||
*
|
||||
* All invalid bookmarks (ie pointing to unknown books, no check is made on bookmark pointing to
|
||||
* invalid articles of valid book) will be migrated (if possible) to a better book.
|
||||
* "Better book", will be determined using method `getBestTargetBookId`.
|
||||
*
|
||||
* @return A tuple<int, int>: <The number of bookmarks updated>, <Number of invalid bookmarks before migration was performed>.
|
||||
*/
|
||||
std::tuple<int, int> migrateBookmarks(MigrationMode migrationMode = ALLOW_DOWNGRADE);
|
||||
|
||||
/**
|
||||
* Migrate all bookmarks associated to a specific book.
|
||||
*
|
||||
* All bookmarks associated to `sourceBookId` book will be migrated to a better book.
|
||||
* "Better book", will be determined using method `getBestTargetBookId`.
|
||||
*
|
||||
* @param sourceBookId the source bookId of the bookmarks to migrate.
|
||||
* @param migrationMode how we will find the best book.
|
||||
* @return The number of bookmarks updated.
|
||||
*/
|
||||
int migrateBookmarks(const std::string& sourceBookId, MigrationMode migrationMode = UPGRADE_ONLY);
|
||||
|
||||
/**
|
||||
* Migrate bookmarks
|
||||
*
|
||||
* Migrate all bookmarks pointing to `source` to `destination`.
|
||||
*
|
||||
* @param sourceBookId the source bookId of the bookmarks to migrate.
|
||||
* @param targetBookId the destination bookId to migrate the bookmarks to.
|
||||
* @return The number of bookmarks updated.
|
||||
*/
|
||||
int migrateBookmarks(const std::string& sourceBookId, const std::string& targetBookId);
|
||||
|
||||
/**
|
||||
* Get the best available bookId for a bookmark.
|
||||
*
|
||||
* Given a bookmark, return the best available bookId.
|
||||
* "best available bookId" is determined using heuristitcs based on book name, flavour and date.
|
||||
*
|
||||
* @param bookmark The bookmark to search the bookId for.
|
||||
* @param migrationMode The migration mode to use.
|
||||
* @return A bookId. Potentially empty string if no suitable book found.
|
||||
*/
|
||||
std::string getBestTargetBookId(const Bookmark& bookmark, MigrationMode migrationMode) const;
|
||||
|
||||
/**
|
||||
* Get the best bookId for a combination of book's name, flavour and date.
|
||||
*
|
||||
* Given a bookName (mandatory), try to find the best book.
|
||||
* If preferedFlavour is given, will try to find a book with the same flavour. If not found, return a book with a different flavour.
|
||||
* If minDate is given, return a book newer than minDate. If not found, return a empty bookId.
|
||||
*
|
||||
* @param bookName The name of the book
|
||||
* @param preferedFlavour The prefered flavour.
|
||||
* @param minDate the minimal book date acceptable. Must be a string in the format "YYYY-MM-DD".
|
||||
* @return A bookId corresponding to the query, or empty string if not found.
|
||||
*/
|
||||
std::string getBestTargetBookId(const std::string& bookName, const std::string& preferedFlavour="", const std::string& minDate="") const;
|
||||
|
||||
// XXX: This is a non-thread-safe operation
|
||||
const Book& getBookById(const std::string& id) const;
|
||||
// XXX: This is a non-thread-safe operation
|
||||
@@ -403,12 +484,13 @@ private: // functions
|
||||
AttributeCounts getBookAttributeCounts(BookStrPropMemFn p) const;
|
||||
std::vector<std::string> getBookPropValueSet(BookStrPropMemFn p) const;
|
||||
BookIdCollection filterViaBookDB(const Filter& filter) const;
|
||||
std::string getBestFromBookCollection(BookIdCollection books, const Bookmark& bookmark, MigrationMode migrationMode) const;
|
||||
unsigned int getBookCount_not_protected(const bool localBooks, const bool remoteBooks) const;
|
||||
void updateBookDB(const Book& book);
|
||||
void dropCache(const std::string& bookId);
|
||||
|
||||
private: //data
|
||||
mutable std::mutex m_mutex;
|
||||
mutable std::recursive_mutex m_mutex;
|
||||
Library::Revision m_revision;
|
||||
std::map<std::string, Entry> m_books;
|
||||
using ArchiveCache = ConcurrentCache<std::string, std::shared_ptr<zim::Archive>>;
|
||||
|
||||
@@ -155,6 +155,15 @@ class Manager
|
||||
const std::string& url = "",
|
||||
const bool checkMetaData = false);
|
||||
|
||||
/**
|
||||
* Add all books from the directory tree into the library.
|
||||
*
|
||||
* @param path The path of the directory to scan.
|
||||
* @param verboseFlag Verbose logs flag.
|
||||
*/
|
||||
void addBooksFromDirectory(const std::string& path,
|
||||
const bool verboseFlag = false);
|
||||
|
||||
std::string writableLibraryPath;
|
||||
|
||||
bool m_hasSearchResult = false;
|
||||
|
||||
@@ -7,10 +7,12 @@ headers = [
|
||||
'downloader.h',
|
||||
'search_renderer.h',
|
||||
'server.h',
|
||||
'spelling_correction.h',
|
||||
'kiwixserve.h',
|
||||
'name_mapper.h',
|
||||
'tools.h',
|
||||
'version.h'
|
||||
'version.h',
|
||||
'i18n.h'
|
||||
]
|
||||
|
||||
install_headers(headers, subdir:'kiwix')
|
||||
|
||||
@@ -50,10 +50,13 @@ class HumanReadableNameMapper : public NameMapper {
|
||||
std::map<std::string, std::string> m_nameToId;
|
||||
|
||||
public:
|
||||
HumanReadableNameMapper(kiwix::Library& library, bool withAlias);
|
||||
HumanReadableNameMapper(const kiwix::Library& library, bool withAlias);
|
||||
virtual ~HumanReadableNameMapper() = default;
|
||||
virtual std::string getNameForId(const std::string& id) const;
|
||||
virtual std::string getIdForName(const std::string& name) const;
|
||||
|
||||
private:
|
||||
void mapName(const kiwix::Library& lib, std::string name, std::string id);
|
||||
};
|
||||
|
||||
class UpdatableNameMapper : public NameMapper {
|
||||
|
||||
@@ -72,6 +72,13 @@ class SearchRenderer
|
||||
this->pageLength = pageLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* set user language
|
||||
*/
|
||||
void setUserLang(const std::string& lang){
|
||||
this->userlang = lang;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the html page with the resutls of the search.
|
||||
*
|
||||
@@ -105,6 +112,7 @@ class SearchRenderer
|
||||
unsigned int pageLength;
|
||||
unsigned int estimatedResultCount;
|
||||
unsigned int resultStart;
|
||||
std::string userlang = "en";
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include "tools.h"
|
||||
|
||||
namespace kiwix
|
||||
{
|
||||
@@ -51,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; }
|
||||
@@ -62,14 +63,19 @@ namespace kiwix
|
||||
{ m_withTaskbar = withTaskbar; m_withLibraryButton = withLibraryButton; }
|
||||
void setBlockExternalLinks(bool blockExternalLinks)
|
||||
{ m_blockExternalLinks = blockExternalLinks; }
|
||||
int getPort();
|
||||
std::string getAddress();
|
||||
void setCatalogOnlyMode(bool enable) { m_catalogOnlyMode = enable; }
|
||||
void setContentServerUrl(std::string url) { m_contentServerUrl = url; }
|
||||
void setIpMode(IpMode mode) { m_ipMode = mode; }
|
||||
int getPort() const;
|
||||
IpAddress getAddress() const;
|
||||
IpMode getIpMode() const;
|
||||
std::vector<std::string> getServerAccessUrls() const;
|
||||
|
||||
protected:
|
||||
std::shared_ptr<Library> mp_library;
|
||||
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;
|
||||
@@ -78,7 +84,10 @@ namespace kiwix
|
||||
bool m_withTaskbar = true;
|
||||
bool m_withLibraryButton = true;
|
||||
bool m_blockExternalLinks = false;
|
||||
IpMode m_ipMode = IpMode::AUTO;
|
||||
int m_ipConnectionLimit = 0;
|
||||
bool m_catalogOnlyMode = false;
|
||||
std::string m_contentServerUrl;
|
||||
std::unique_ptr<InternalServer> mp_server;
|
||||
};
|
||||
}
|
||||
|
||||
58
include/spelling_correction.h
Normal file
58
include/spelling_correction.h
Normal file
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Veloman Yunkan
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||
* MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef KIWIX_SPELLING_CORRECTION_H
|
||||
#define KIWIX_SPELLING_CORRECTION_H
|
||||
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace zim
|
||||
{
|
||||
class Archive;
|
||||
}
|
||||
|
||||
namespace Xapian
|
||||
{
|
||||
class Database;
|
||||
}
|
||||
|
||||
namespace kiwix
|
||||
{
|
||||
|
||||
class SpellingsDB
|
||||
{
|
||||
public: // functions
|
||||
SpellingsDB(const zim::Archive& archive, std::filesystem::path cacheDirPath);
|
||||
~SpellingsDB();
|
||||
|
||||
SpellingsDB(const SpellingsDB& ) = delete;
|
||||
void operator=(const SpellingsDB& ) = delete;
|
||||
|
||||
std::vector<std::string> getSpellingCorrections(const std::string& word, uint32_t maxCount) const;
|
||||
|
||||
private: // data
|
||||
std::unique_ptr<Xapian::Database> impl_;
|
||||
};
|
||||
|
||||
} // namespace kiwix
|
||||
|
||||
#endif // KIWIX_SPELLING_CORRECTION_H
|
||||
@@ -24,8 +24,17 @@
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <cstdint>
|
||||
#include "common.h"
|
||||
|
||||
namespace kiwix
|
||||
{
|
||||
|
||||
struct IpAddress
|
||||
{
|
||||
std::string addr; // IPv4 address
|
||||
std::string addr6; // IPv6 address
|
||||
};
|
||||
|
||||
namespace kiwix {
|
||||
typedef std::pair<std::string, std::string> LangNameCodePair;
|
||||
typedef std::vector<LangNameCodePair> FeedLanguages;
|
||||
typedef std::vector<std::string> FeedCategories;
|
||||
@@ -37,26 +46,6 @@ typedef std::vector<std::string> FeedCategories;
|
||||
*/
|
||||
std::string getCurrentDirectory();
|
||||
|
||||
/**
|
||||
* Return the data directory.
|
||||
*
|
||||
* The data directory is a directory where to put data (zim files, ...)
|
||||
* It depends of the platform and it may be changed by user using environment variable.
|
||||
*
|
||||
* The resolution order is :
|
||||
* - `KIWIX_DATA_DIR` env variable (if set).
|
||||
* - On Windows :
|
||||
* . `$APPDATA/kiwix` if $APPDATA is set
|
||||
* . `$USERPROFILE/kiwix` if $USERPROFILE is set
|
||||
* - Else :
|
||||
* . `$XDG_DATA_HOME/kiwix`if $XDG_DATA_HOME is set
|
||||
* . `$HOME/.local/share/kiwx` if $HOWE is set
|
||||
* - current directory
|
||||
*
|
||||
* @return the path of the data directory (utf8 encoded)
|
||||
*/
|
||||
std::string getDataDirectory();
|
||||
|
||||
/** Return the path of the executable
|
||||
*
|
||||
* Some application may be packaged in auto extractible archive (Appimage) and the
|
||||
@@ -210,13 +199,30 @@ bool fileReadable(const std::string& path);
|
||||
std::string getMimeTypeForFile(const std::string& filename);
|
||||
|
||||
/** Provides all available network interfaces
|
||||
*
|
||||
* This function provides the available IPv4 and IPv6 network interfaces
|
||||
* as a map from the interface name to its IPv4 and/or IPv6 address(es).
|
||||
*/
|
||||
std::map<std::string, IpAddress> getNetworkInterfacesIPv4Or6();
|
||||
|
||||
/** Provides all available IPv4 network interfaces
|
||||
*
|
||||
* This function provides the available IPv4 network interfaces
|
||||
* as a map from the interface name to its IPv4 address.
|
||||
*
|
||||
* Provided for backward compatibility with libkiwix v13.1.0.
|
||||
*/
|
||||
std::map<std::string, std::string> getNetworkInterfaces();
|
||||
|
||||
/** Provides the best IP address
|
||||
* This function provides the best IP address from the list given by getNetworkInterfaces
|
||||
* This function provides the best IP addresses for both ipv4 and ipv6 protocols,
|
||||
* in an IpAddress struct, based on the list given by getNetworkInterfacesIPv4Or6()
|
||||
*/
|
||||
IpAddress getBestPublicIps();
|
||||
|
||||
/** Provides the best IPv4 adddress
|
||||
* Equivalent to getBestPublicIp(false). Provided for backward compatibility
|
||||
* with libkiwix v13.1.0.
|
||||
*/
|
||||
std::string getBestPublicIp();
|
||||
|
||||
@@ -231,15 +237,15 @@ std::string beautifyFileSize(uint64_t number);
|
||||
|
||||
/**
|
||||
* Load languages stored in an OPDS stream.
|
||||
*
|
||||
*
|
||||
* @param content the OPDS stream.
|
||||
* @return vector containing pairs of language code and their corresponding full language name.
|
||||
* @return vector containing pairs of language code and their corresponding full language name.
|
||||
*/
|
||||
FeedLanguages readLanguagesFromFeed(const std::string& content);
|
||||
|
||||
/**
|
||||
* Load categories stored in an OPDS stream .
|
||||
*
|
||||
*
|
||||
* @param content the OPDS stream.
|
||||
* @return vector containing category strings.
|
||||
*/
|
||||
@@ -247,10 +253,19 @@ FeedCategories readCategoriesFromFeed(const std::string& content);
|
||||
|
||||
/**
|
||||
* Retrieve the full language name associated with a given ISO 639-3 language code.
|
||||
*
|
||||
*
|
||||
* @param lang ISO 639-3 language code.
|
||||
* @return full language name.
|
||||
*/
|
||||
std::string getLanguageSelfName(const std::string& lang);
|
||||
|
||||
/**
|
||||
* Slugifies the filename by converting any characters reserved by the operating
|
||||
* system to '_'. Note filename is only the file name and not a path.
|
||||
*
|
||||
* @param filename Valid UTF-8 encoded file name string.
|
||||
* @return slugified string.
|
||||
*/
|
||||
std::string getSlugifiedFileName(const std::string& filename);
|
||||
}
|
||||
#endif // KIWIX_TOOLS_H
|
||||
|
||||
10
kiwix.pc.in
10
kiwix.pc.in
@@ -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@
|
||||
97
meson.build
97
meson.build
@@ -1,26 +1,61 @@
|
||||
project('libkiwix', 'cpp',
|
||||
version : '13.0.0',
|
||||
version : '14.1.1',
|
||||
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)
|
||||
endif
|
||||
|
||||
libicu_dep = dependency('icu-i18n', static:static_deps)
|
||||
if libicu_dep.version().version_compare('>= 76')
|
||||
libicu_deps = [libicu_dep, dependency('icu-uc', static:static_deps)]
|
||||
else
|
||||
libicu_deps = [libicu_dep]
|
||||
endif
|
||||
|
||||
pugixml_dep = dependency('pugixml', static:static_deps)
|
||||
libcurl_dep = dependency('libcurl', static:static_deps)
|
||||
microhttpd_dep = dependency('libmicrohttpd', static:static_deps)
|
||||
@@ -35,9 +70,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.4.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
|
||||
|
||||
|
||||
@@ -49,21 +85,25 @@ endif
|
||||
|
||||
if host_machine.system() == 'windows'
|
||||
add_project_arguments('-DNOMINMAX', language: 'cpp')
|
||||
extra_libs += ['-liphlpapi']
|
||||
endif
|
||||
|
||||
all_deps = [thread_dep, libicu_dep, libzim_dep, pugixml_dep, libcurl_dep, microhttpd_dep, zlib_dep, xapian_dep]
|
||||
if build_machine.system() == 'windows'
|
||||
extra_libs += ['-lshlwapi', '-lwinmm']
|
||||
endif
|
||||
|
||||
|
||||
# Dependencies as string
|
||||
all_deps = [thread_dep, libzim_dep, pugixml_dep, libcurl_dep, microhttpd_dep, zlib_dep, xapian_dep]
|
||||
|
||||
# Dependencies as array
|
||||
all_deps += libicu_deps
|
||||
|
||||
inc = include_directories('include', extra_include)
|
||||
|
||||
conf = configuration_data()
|
||||
conf.set('LIBKIWIX_VERSION', '"@0@"'.format(meson.project_version()))
|
||||
|
||||
if build_machine.system() == 'windows'
|
||||
extra_link_args = ['-lshlwapi', '-lwinmm']
|
||||
else
|
||||
extra_link_args = []
|
||||
endif
|
||||
|
||||
subdir('include')
|
||||
subdir('scripts')
|
||||
subdir('static')
|
||||
@@ -73,17 +113,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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "xmlrpc.h"
|
||||
#include <iostream>
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <thread>
|
||||
#include <chrono>
|
||||
@@ -29,18 +30,41 @@
|
||||
|
||||
namespace kiwix {
|
||||
|
||||
Aria2::Aria2():
|
||||
namespace {
|
||||
|
||||
void pauseAnyActiveDownloads(const std::string& ariaSessionFilePath)
|
||||
{
|
||||
std::ifstream inputFile(ariaSessionFilePath);
|
||||
if ( !inputFile )
|
||||
return;
|
||||
|
||||
std::ostringstream ss;
|
||||
std::string line;
|
||||
while ( std::getline(inputFile, line) ) {
|
||||
if ( !startsWith(line, " pause=") ) {
|
||||
ss << line << "\n";
|
||||
}
|
||||
if ( !line.empty() && line[0] != ' ' && line[0] != '#' ) {
|
||||
ss << " pause=true\n";
|
||||
}
|
||||
}
|
||||
|
||||
std::ofstream outputFile(ariaSessionFilePath);
|
||||
outputFile << ss.str();
|
||||
}
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
Aria2::Aria2(std::string sessionFileDir):
|
||||
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;
|
||||
// std::string log_dir = "--log=\"" + logDir + "\"";
|
||||
@@ -67,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());
|
||||
}
|
||||
@@ -97,20 +120,30 @@ Aria2::Aria2():
|
||||
curl_easy_setopt(p_curl, CURLOPT_PORT, m_port);
|
||||
curl_easy_setopt(p_curl, CURLOPT_POST, 1L);
|
||||
curl_easy_setopt(p_curl, CURLOPT_ERRORBUFFER, curlErrorBuffer);
|
||||
curl_easy_setopt(p_curl, CURLOPT_TIMEOUT_MS, 100);
|
||||
|
||||
int watchdog = 50;
|
||||
while(--watchdog) {
|
||||
typedef std::chrono::duration<double> Seconds;
|
||||
|
||||
const double MAX_WAITING_TIME_SECONDS = 1;
|
||||
const auto t0 = std::chrono::steady_clock::now();
|
||||
bool maxWaitingTimeWasExceeded = false;
|
||||
|
||||
CURLcode res = CURLE_OK;
|
||||
while ( !maxWaitingTimeWasExceeded ) {
|
||||
sleep(10);
|
||||
curlErrorBuffer[0] = 0;
|
||||
auto res = curl_easy_perform(p_curl);
|
||||
res = curl_easy_perform(p_curl);
|
||||
if (res == CURLE_OK) {
|
||||
break;
|
||||
} else if (watchdog == 1) {
|
||||
LOG_ARIA_ERROR();
|
||||
}
|
||||
|
||||
const auto dt = std::chrono::steady_clock::now() - t0;
|
||||
const double elapsedTime = std::chrono::duration_cast<Seconds>(dt).count();
|
||||
maxWaitingTimeWasExceeded = elapsedTime > MAX_WAITING_TIME_SECONDS;
|
||||
}
|
||||
curl_easy_cleanup(p_curl);
|
||||
if (!watchdog) {
|
||||
if ( maxWaitingTimeWasExceeded ) {
|
||||
LOG_ARIA_ERROR();
|
||||
throw std::runtime_error("Cannot connect to aria2c rpc. Aria2c launch cmd : " + launchCmd);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -82,10 +82,11 @@ void Book::update(const zim::Archive& archive) {
|
||||
m_size = static_cast<uint64_t>(getArchiveFileSize(archive)) << 10;
|
||||
|
||||
m_illustrations.clear();
|
||||
for ( const auto illustrationSize : archive.getIllustrationSizes() ) {
|
||||
for ( const auto& illustrationInfo : archive.getIllustrationInfos() ) {
|
||||
const auto illustration = std::make_shared<Illustration>();
|
||||
const zim::Item illustrationItem = archive.getIllustrationItem(illustrationSize);
|
||||
illustration->width = illustration->height = illustrationSize;
|
||||
const zim::Item illustrationItem = archive.getIllustrationItem(illustrationInfo);
|
||||
illustration->width = illustrationInfo.width;
|
||||
illustration->height = illustrationInfo.height;
|
||||
illustration->mimeType = illustrationItem.getMimetype();
|
||||
illustration->data = illustrationItem.getData();
|
||||
// NOTE: illustration->url is left uninitialized
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
*/
|
||||
|
||||
#include "bookmark.h"
|
||||
#include "book.h"
|
||||
|
||||
#include <pugixml.hpp>
|
||||
|
||||
@@ -28,6 +29,17 @@ Bookmark::Bookmark()
|
||||
{
|
||||
}
|
||||
|
||||
Bookmark::Bookmark(const Book& book, const std::string& path, const std::string& title):
|
||||
m_bookId(book.getId()),
|
||||
m_bookTitle(book.getTitle()),
|
||||
m_bookName(book.getName()),
|
||||
m_bookFlavour(book.getFlavour()),
|
||||
m_url(path),
|
||||
m_title(title),
|
||||
m_language(book.getCommaSeparatedLanguages()),
|
||||
m_date(book.getDate())
|
||||
{}
|
||||
|
||||
/* Destructor */
|
||||
Bookmark::~Bookmark()
|
||||
{
|
||||
@@ -38,6 +50,8 @@ void Bookmark::updateFromXml(const pugi::xml_node& node)
|
||||
auto bookNode = node.child("book");
|
||||
m_bookId = bookNode.child("id").child_value();
|
||||
m_bookTitle = bookNode.child("title").child_value();
|
||||
m_bookName = bookNode.child("name").child_value();
|
||||
m_bookFlavour = bookNode.child("flavour").child_value();
|
||||
m_language = bookNode.child("language").child_value();
|
||||
m_date = bookNode.child("date").child_value();
|
||||
m_title = node.child("title").child_value();
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
*/
|
||||
|
||||
#include "downloader.h"
|
||||
#include "tools.h"
|
||||
#include "tools/pathTools.h"
|
||||
#include "tools/stringTools.h"
|
||||
|
||||
@@ -124,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()) {
|
||||
@@ -150,11 +151,20 @@ Downloader::Downloader() :
|
||||
/* Destructor */
|
||||
Downloader::~Downloader()
|
||||
{
|
||||
close();
|
||||
}
|
||||
|
||||
void Downloader::close()
|
||||
{
|
||||
mp_aria->close();
|
||||
if ( mp_aria ) {
|
||||
try {
|
||||
mp_aria->close();
|
||||
} catch (const std::exception& err) {
|
||||
std::cerr << "ERROR: Failed to save the downloader state: "
|
||||
<< err.what() << std::endl;
|
||||
}
|
||||
mp_aria.reset();
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> Downloader::getDownloadIds() const {
|
||||
@@ -166,13 +176,49 @@ std::vector<std::string> Downloader::getDownloadIds() const {
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::shared_ptr<Download> Downloader::startDownload(const std::string& uri, const std::vector<std::pair<std::string, std::string>>& options)
|
||||
namespace
|
||||
{
|
||||
|
||||
bool downloadCanBeReused(const Download& d,
|
||||
const std::string& uri,
|
||||
const Downloader::Options& /*options*/)
|
||||
{
|
||||
const auto& uris = d.getUris();
|
||||
const bool sameURI = std::find(uris.begin(), uris.end(), uri) != uris.end();
|
||||
|
||||
if ( !sameURI )
|
||||
return false;
|
||||
|
||||
switch ( d.getStatus() ) {
|
||||
case Download::K_ERROR:
|
||||
case Download::K_UNKNOWN:
|
||||
case Download::K_REMOVED:
|
||||
return false;
|
||||
|
||||
case Download::K_ACTIVE:
|
||||
case Download::K_WAITING:
|
||||
case Download::K_PAUSED:
|
||||
return true; // XXX: what if options are different?
|
||||
|
||||
case Download::K_COMPLETE:
|
||||
return fileExists(d.getPath()); // XXX: what if options are different?
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
std::shared_ptr<Download> Downloader::startDownload(const std::string& uri, const std::string& downloadDir, Options options)
|
||||
{
|
||||
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;
|
||||
auto& uris = d->getUris();
|
||||
if (std::find(uris.begin(), uris.end(), uri) != uris.end())
|
||||
if ( downloadCanBeReused(*d, uri, options) )
|
||||
return d;
|
||||
}
|
||||
std::vector<std::string> uris = {uri};
|
||||
|
||||
@@ -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}
|
||||
@@ -107,6 +130,7 @@ std::string HTMLDumper::dumpPlainHTML(kiwix::Filter filter) const
|
||||
RESOURCE::templates::no_js_library_page_html,
|
||||
kainjow::mustache::object{
|
||||
{"root", rootLocation},
|
||||
{"contentAccessUrl", onlyAsNonEmptyMustacheValue(contentAccessUrl)},
|
||||
{"books", booksData },
|
||||
{"searchQuery", searchQuery},
|
||||
{"languages", languages},
|
||||
|
||||
213
src/library.cpp
213
src/library.cpp
@@ -110,7 +110,7 @@ Library::~Library() = default;
|
||||
|
||||
bool Library::addBook(const Book& book)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
++m_revision;
|
||||
/* Try to find it */
|
||||
updateBookDB(book);
|
||||
@@ -141,13 +141,13 @@ bool Library::addBook(const Book& book)
|
||||
|
||||
void Library::addBookmark(const Bookmark& bookmark)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
m_bookmarks.push_back(bookmark);
|
||||
}
|
||||
|
||||
bool Library::removeBookmark(const std::string& zimId, const std::string& url)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
for(auto it=m_bookmarks.begin(); it!=m_bookmarks.end(); it++) {
|
||||
if (it->getBookId() == zimId && it->getUrl() == url) {
|
||||
m_bookmarks.erase(it);
|
||||
@@ -157,6 +157,159 @@ bool Library::removeBookmark(const std::string& zimId, const std::string& url)
|
||||
return false;
|
||||
}
|
||||
|
||||
std::tuple<int, int> Library::migrateBookmarks(MigrationMode migrationMode) {
|
||||
std::set<std::string> sourceBooks;
|
||||
int invalidBookmarks = 0;
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
for(auto& bookmark:m_bookmarks) {
|
||||
if (m_books.find(bookmark.getBookId()) == m_books.end()) {
|
||||
invalidBookmarks += 1;
|
||||
sourceBooks.insert(bookmark.getBookId());
|
||||
}
|
||||
}
|
||||
}
|
||||
int changed = 0;
|
||||
for(auto& sourceBook:sourceBooks) {
|
||||
changed += migrateBookmarks(sourceBook, migrationMode);
|
||||
}
|
||||
return std::make_tuple(changed, invalidBookmarks);
|
||||
}
|
||||
|
||||
std::string Library::getBestFromBookCollection(BookIdCollection books, const Bookmark& bookmark, MigrationMode migrationMode) const {
|
||||
// This function try to get the best book for a bookmark from a book collection.
|
||||
// It assumes that all books in the collection are "acceptable".
|
||||
// (this definiton is not clear but for now it is book's name is equal to bookmark's bookName)
|
||||
//
|
||||
// The algorithm first sort the colletion by "flavour equality" and date.
|
||||
// "flavour equality" is if book's flavour is same that bookmark's flavour (let's say "flavourA" here)
|
||||
// So we have the sorted collection:
|
||||
// - flavourA, date 5
|
||||
// - flavourA, date 4
|
||||
// - flavourB, date 6
|
||||
// - flavourC, date 5
|
||||
// - flavourB, date 3
|
||||
//
|
||||
// Then, depending of migrationMode:
|
||||
// - If ALLOW_DOWNGRADE => take the first one
|
||||
// - If UPGRADE_ONLY => loop on books until we find a book newer than bookmark.
|
||||
// So if bookmark date is 5 => flavourB, date 6
|
||||
// if bookmark date is 4 => flavourA, date 5
|
||||
// if bookmark date is 7 => No book
|
||||
|
||||
if (books.empty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
sort(books, DATE, false);
|
||||
stable_sort(books.begin(), books.end(), [&](const std::string& bookId1, const std::string& bookId2) {
|
||||
const auto& book1 = getBookById(bookId1);
|
||||
const auto& book2 = getBookById(bookId2);
|
||||
bool same_flavour1 = book1.getFlavour() == bookmark.getBookFlavour();
|
||||
bool same_flavour2 = book2.getFlavour() == bookmark.getBookFlavour();
|
||||
// return True if bookId1 is before bookId2, ie if same_flavour1 and not same_flavour2
|
||||
return same_flavour1 > same_flavour2;
|
||||
});
|
||||
|
||||
if (migrationMode == ALLOW_DOWNGRADE) {
|
||||
return books[0];
|
||||
} else {
|
||||
for (const auto& bookId: books) {
|
||||
const auto& book = getBookById(bookId);
|
||||
if (book.getDate() >= bookmark.getDate()) {
|
||||
return bookId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string remove_quote(std::string input) {
|
||||
std::replace(input.begin(), input.end(), '"', ' ');
|
||||
return input;
|
||||
}
|
||||
|
||||
std::string Library::getBestTargetBookId(const std::string& bookName, const std::string& preferedFlavour, const std::string& minDate) const {
|
||||
// Let's reuse our algorithm based on bookmark.
|
||||
MigrationMode migrationMode = UPGRADE_ONLY;
|
||||
auto bookmark = Bookmark();
|
||||
bookmark.setBookName(bookName);
|
||||
bookmark.setBookFlavour(preferedFlavour);
|
||||
|
||||
if (minDate.empty()) {
|
||||
migrationMode = ALLOW_DOWNGRADE;
|
||||
} else {
|
||||
bookmark.setDate(minDate);
|
||||
}
|
||||
|
||||
return getBestTargetBookId(bookmark, migrationMode);
|
||||
}
|
||||
|
||||
std::string Library::getBestTargetBookId(const Bookmark& bookmark, MigrationMode migrationMode) const {
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
// Search for a existing book with the same name
|
||||
auto book_filter = Filter();
|
||||
if (!bookmark.getBookName().empty()) {
|
||||
book_filter.name(bookmark.getBookName());
|
||||
} else {
|
||||
// We don't have a name stored (older bookmarks)
|
||||
// Fallback on title (All bookmarks should have one, but let's be safe against wrongly filled bookmark)
|
||||
if (bookmark.getBookTitle().empty()) {
|
||||
// No bookName nor bookTitle, no way to find target book.
|
||||
return "";
|
||||
}
|
||||
book_filter.query("title:\"" + remove_quote(bookmark.getBookTitle()) + "\"");
|
||||
}
|
||||
auto targetBooks = filter(book_filter);
|
||||
auto bestBook = getBestFromBookCollection(targetBooks, bookmark, migrationMode);
|
||||
if (bestBook.empty()) {
|
||||
try {
|
||||
getBookById(bookmark.getBookId());
|
||||
return bookmark.getBookId();
|
||||
} catch (std::out_of_range&) {}
|
||||
}
|
||||
return bestBook;
|
||||
}
|
||||
|
||||
int Library::migrateBookmarks(const std::string& sourceBookId, MigrationMode migrationMode) {
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
|
||||
Bookmark firstBookmarkToChange;
|
||||
for(auto& bookmark:m_bookmarks) {
|
||||
if (bookmark.getBookId() == sourceBookId) {
|
||||
firstBookmarkToChange = bookmark;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (firstBookmarkToChange.getBookId().empty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::string betterBook = getBestTargetBookId(firstBookmarkToChange, migrationMode);
|
||||
|
||||
if (betterBook.empty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return migrateBookmarks(sourceBookId, betterBook);
|
||||
}
|
||||
|
||||
int Library::migrateBookmarks(const std::string& sourceBookId, const std::string& targetBookId) {
|
||||
if (sourceBookId == targetBookId) {
|
||||
return 0;
|
||||
}
|
||||
int changed = 0;
|
||||
for (auto& bookmark:m_bookmarks) {
|
||||
if (bookmark.getBookId() == sourceBookId) {
|
||||
bookmark.setBookId(targetBookId);
|
||||
changed +=1;
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
|
||||
void Library::dropCache(const std::string& id)
|
||||
{
|
||||
@@ -166,7 +319,7 @@ void Library::dropCache(const std::string& id)
|
||||
|
||||
bool Library::removeBookById(const std::string& id)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
m_bookDB->delete_document("Q" + id);
|
||||
dropCache(id);
|
||||
// We do not change the cache size here
|
||||
@@ -184,7 +337,7 @@ bool Library::removeBookById(const std::string& id)
|
||||
|
||||
Library::Revision Library::getRevision() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
return m_revision;
|
||||
}
|
||||
|
||||
@@ -192,7 +345,7 @@ uint32_t Library::removeBooksNotUpdatedSince(Revision libraryRevision)
|
||||
{
|
||||
BookIdCollection booksToRemove;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
for ( const auto& entry : m_books) {
|
||||
if ( entry.second.lastUpdatedRevision <= libraryRevision ) {
|
||||
booksToRemove.push_back(entry.first);
|
||||
@@ -217,7 +370,7 @@ const Book& Library::getBookById(const std::string& id) const
|
||||
|
||||
Book Library::getBookByIdThreadSafe(const std::string& id) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
return getBookById(id);
|
||||
}
|
||||
|
||||
@@ -275,7 +428,7 @@ std::shared_ptr<ZimSearcher> Library::getSearcherByIds(const BookIdSet& ids)
|
||||
unsigned int Library::getBookCount(const bool localBooks,
|
||||
const bool remoteBooks) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
return getBookCount_not_protected(localBooks, remoteBooks);
|
||||
}
|
||||
|
||||
@@ -288,7 +441,7 @@ bool Library::writeToFile(const std::string& path) const
|
||||
dumper.setBaseDir(baseDir);
|
||||
std::string xml;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
xml = dumper.dumpLibXMLContent(allBookIds);
|
||||
};
|
||||
return writeTextFile(path, xml);
|
||||
@@ -304,7 +457,7 @@ bool Library::writeBookmarksToFile(const std::string& path) const
|
||||
|
||||
Library::AttributeCounts Library::getBookAttributeCounts(BookStrPropMemFn p) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
AttributeCounts propValueCounts;
|
||||
|
||||
for (const auto& pair: m_books) {
|
||||
@@ -336,7 +489,7 @@ std::vector<std::string> Library::getBooksLanguages() const
|
||||
|
||||
Library::AttributeCounts Library::getBooksLanguagesWithCounts() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
AttributeCounts langsWithCounts;
|
||||
|
||||
for (const auto& pair: m_books) {
|
||||
@@ -352,7 +505,7 @@ Library::AttributeCounts Library::getBooksLanguagesWithCounts() const
|
||||
|
||||
std::vector<std::string> Library::getBooksCategories() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
std::set<std::string> categories;
|
||||
|
||||
for (const auto& pair: m_books) {
|
||||
@@ -383,7 +536,7 @@ const std::vector<kiwix::Bookmark> Library::getBookmarks(bool onlyValidBookmarks
|
||||
}
|
||||
std::vector<kiwix::Bookmark> validBookmarks;
|
||||
auto booksId = getBooksIds();
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
for(auto& bookmark:m_bookmarks) {
|
||||
if (std::find(booksId.begin(), booksId.end(), bookmark.getBookId()) != booksId.end()) {
|
||||
validBookmarks.push_back(bookmark);
|
||||
@@ -394,7 +547,7 @@ const std::vector<kiwix::Bookmark> Library::getBookmarks(bool onlyValidBookmarks
|
||||
|
||||
Library::BookIdCollection Library::getBooksIds() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
BookIdCollection bookIds;
|
||||
|
||||
for (auto& pair: m_books) {
|
||||
@@ -437,6 +590,7 @@ void Library::updateBookDB(const Book& book)
|
||||
indexer.index_text(normalizeText(book.getCreator()), 1, "A");
|
||||
indexer.index_text(normalizeText(book.getPublisher()), 1, "XP");
|
||||
doc.add_term("XN"+normalizeText(book.getName()));
|
||||
indexer.index_text(normalizeText(book.getFlavour()), 1, "XF");
|
||||
indexer.index_text(normalizeText(book.getCategory()), 1, "XC");
|
||||
|
||||
for ( const auto& tag : split(normalizeText(book.getTags()), ";") ) {
|
||||
@@ -477,6 +631,7 @@ Xapian::Query buildXapianQueryFromFilterQuery(const Filter& filter)
|
||||
queryParser.add_prefix("title", "S");
|
||||
queryParser.add_prefix("description", "XD");
|
||||
queryParser.add_prefix("name", "XN");
|
||||
queryParser.add_prefix("flavour", "XF");
|
||||
queryParser.add_prefix("category", "XC");
|
||||
queryParser.add_prefix("lang", "L");
|
||||
queryParser.add_prefix("publisher", "XP");
|
||||
@@ -490,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;
|
||||
@@ -503,6 +656,11 @@ Xapian::Query nameQuery(const std::string& name)
|
||||
return Xapian::Query("XN" + normalizeText(name));
|
||||
}
|
||||
|
||||
Xapian::Query flavourQuery(const std::string& name)
|
||||
{
|
||||
return Xapian::Query("XF" + normalizeText(name));
|
||||
}
|
||||
|
||||
Xapian::Query multipleParamQuery(const std::string& commaSeparatedList, const std::string& prefix)
|
||||
{
|
||||
Xapian::Query q;
|
||||
@@ -570,6 +728,9 @@ Xapian::Query buildXapianQuery(const Filter& filter)
|
||||
if ( filter.hasName() ) {
|
||||
q = Xapian::Query(Xapian::Query::OP_AND, q, nameQuery(filter.getName()));
|
||||
}
|
||||
if ( filter.hasFlavour() ) {
|
||||
q = Xapian::Query(Xapian::Query::OP_AND, q, flavourQuery(filter.getFlavour()));
|
||||
}
|
||||
if ( filter.hasCategory() ) {
|
||||
q = Xapian::Query(Xapian::Query::OP_AND, q, categoryQuery(filter.getCategory()));
|
||||
}
|
||||
@@ -600,7 +761,7 @@ Library::BookIdCollection Library::filterViaBookDB(const Filter& filter) const
|
||||
|
||||
BookIdCollection bookIds;
|
||||
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
Xapian::Enquire enquire(*m_bookDB);
|
||||
enquire.set_query(query);
|
||||
const auto results = enquire.get_mset(0, m_books.size());
|
||||
@@ -615,7 +776,7 @@ Library::BookIdCollection Library::filter(const Filter& filter) const
|
||||
{
|
||||
BookIdCollection result;
|
||||
const auto preliminaryResult = filterViaBookDB(filter);
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
for(auto id : preliminaryResult) {
|
||||
if(filter.accept(m_books.at(id))) {
|
||||
result.push_back(id);
|
||||
@@ -689,7 +850,7 @@ void Library::sort(BookIdCollection& bookIds, supportedListSortBy sort, bool asc
|
||||
// NOTE: for the entire duration of the sort. Will need to obtain (under a
|
||||
// NOTE: lock) the required atributes from the books once, and then the
|
||||
// NOTE: sorting will run on a copy of data without locking.
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
switch(sort) {
|
||||
case TITLE:
|
||||
std::sort(bookIds.begin(), bookIds.end(), Comparator<TITLE>(this, ascending));
|
||||
@@ -735,6 +896,7 @@ enum filterTypes {
|
||||
QUERY = FLAG(12),
|
||||
NAME = FLAG(13),
|
||||
CATEGORY = FLAG(14),
|
||||
FLAVOUR = FLAG(15),
|
||||
};
|
||||
|
||||
Filter& Filter::local(bool accept)
|
||||
@@ -836,6 +998,13 @@ Filter& Filter::name(std::string name)
|
||||
activeFilters |= NAME;
|
||||
return *this;
|
||||
}
|
||||
|
||||
Filter& Filter::flavour(std::string flavour)
|
||||
{
|
||||
_flavour = flavour;
|
||||
activeFilters |= FLAVOUR;
|
||||
return *this;
|
||||
}
|
||||
|
||||
Filter& Filter::clearLang()
|
||||
{
|
||||
@@ -881,6 +1050,12 @@ bool Filter::hasCreator() const
|
||||
return ACTIVE(_CREATOR);
|
||||
}
|
||||
|
||||
bool Filter::hasFlavour() const
|
||||
{
|
||||
return ACTIVE(FLAVOUR);
|
||||
}
|
||||
|
||||
|
||||
bool Filter::accept(const Book& book) const
|
||||
{
|
||||
auto local = !book.getPath().empty();
|
||||
|
||||
@@ -50,6 +50,13 @@ class LibraryDumper
|
||||
*/
|
||||
void setRootLocation(const std::string& rootLocation) { this->rootLocation = rootLocation; }
|
||||
|
||||
/**
|
||||
* Set the URL for accessing book content
|
||||
*
|
||||
* @param url the URL of the /content endpoint of the content server
|
||||
*/
|
||||
void setContentAccessUrl(const std::string& url) { this->contentAccessUrl = url; }
|
||||
|
||||
/**
|
||||
* Set some informations about the search results.
|
||||
*
|
||||
@@ -58,10 +65,10 @@ class LibraryDumper
|
||||
* @param count the number of result of the current set (or page).
|
||||
*/
|
||||
void setOpenSearchInfo(int totalResult, int startIndex, int count);
|
||||
|
||||
|
||||
/**
|
||||
* Sets user default language
|
||||
*
|
||||
*
|
||||
* @param userLang the user language to be set
|
||||
*/
|
||||
void setUserLanguage(std::string userLang) { this->m_userLang = userLang; }
|
||||
@@ -81,6 +88,7 @@ class LibraryDumper
|
||||
const kiwix::NameMapper* const nameMapper;
|
||||
std::string libraryId;
|
||||
std::string rootLocation;
|
||||
std::string contentAccessUrl;
|
||||
std::string m_userLang;
|
||||
int m_totalResults;
|
||||
int m_startIndex;
|
||||
|
||||
@@ -97,11 +97,15 @@ void LibXMLDumper::handleBookmark(Bookmark bookmark, pugi::xml_node root_node) {
|
||||
auto book = library->getBookByIdThreadSafe(bookmark.getBookId());
|
||||
ADD_TEXT_ENTRY(book_node, "id", book.getId());
|
||||
ADD_TEXT_ENTRY(book_node, "title", book.getTitle());
|
||||
ADD_TEXT_ENTRY(book_node, "name", book.getName());
|
||||
ADD_TEXT_ENTRY(book_node, "flavour", book.getFlavour());
|
||||
ADD_TEXT_ENTRY(book_node, "language", book.getCommaSeparatedLanguages());
|
||||
ADD_TEXT_ENTRY(book_node, "date", book.getDate());
|
||||
} catch (...) {
|
||||
ADD_TEXT_ENTRY(book_node, "id", bookmark.getBookId());
|
||||
ADD_TEXT_ENTRY(book_node, "title", bookmark.getBookTitle());
|
||||
ADD_TEXT_ENTRY(book_node, "name", bookmark.getBookName());
|
||||
ADD_TEXT_ENTRY(book_node, "flavour", bookmark.getBookFlavour());
|
||||
ADD_TEXT_ENTRY(book_node, "language", bookmark.getLanguage());
|
||||
ADD_TEXT_ENTRY(book_node, "date", bookmark.getDate());
|
||||
}
|
||||
@@ -135,7 +139,7 @@ std::string LibXMLDumper::dumpLibXMLBookmark()
|
||||
pugi::xml_node bookmarksNode = doc.append_child("bookmarks");
|
||||
|
||||
if (library) {
|
||||
for (auto& bookmark: library->getBookmarks()) {
|
||||
for (auto& bookmark: library->getBookmarks(false)) {
|
||||
handleBookmark(bookmark, bookmarksNode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,14 @@
|
||||
#include "tools/pathTools.h"
|
||||
|
||||
#include <pugixml.hpp>
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <set>
|
||||
#include <queue>
|
||||
#include <cctype>
|
||||
#include <algorithm>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace kiwix
|
||||
{
|
||||
@@ -251,6 +259,58 @@ bool Manager::addBookFromPath(const std::string& pathToOpen,
|
||||
.empty());
|
||||
}
|
||||
|
||||
void Manager::addBooksFromDirectory(const std::string& path,
|
||||
const bool verboseFlag)
|
||||
{
|
||||
std::set<std::string> iteratedDirs;
|
||||
std::queue<std::string> dirQueue;
|
||||
dirQueue.push(fs::absolute(path).u8string());
|
||||
int totalBooksAdded = 0;
|
||||
if (verboseFlag)
|
||||
std::cout << "Adding books from the directory tree: " << dirQueue.front() << std::endl;
|
||||
|
||||
while (!dirQueue.empty()) {
|
||||
const auto currentPath = dirQueue.front();
|
||||
dirQueue.pop();
|
||||
if (verboseFlag)
|
||||
std::cout << "Visiting directory: " << currentPath << std::endl;
|
||||
for (const auto& dirEntry : fs::directory_iterator(currentPath)) {
|
||||
auto resolvedPath = dirEntry.path();
|
||||
if (fs::is_symlink(dirEntry)) {
|
||||
try {
|
||||
resolvedPath = fs::canonical(dirEntry.path());
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Could not resolve symlink " << resolvedPath.u8string() << " to a valid path. Skipping..." << std::endl;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const std::string pathString = resolvedPath.u8string();
|
||||
std::string resolvedPathExtension = resolvedPath.extension();
|
||||
std::transform(resolvedPathExtension.begin(), resolvedPathExtension.end(), resolvedPathExtension.begin(),
|
||||
[](unsigned char c){ return std::tolower(c); });
|
||||
if (fs::is_directory(resolvedPath)) {
|
||||
if (iteratedDirs.find(pathString) == iteratedDirs.end())
|
||||
dirQueue.push(pathString);
|
||||
else if (verboseFlag)
|
||||
std::cout << "Already iterated over " << pathString << ". Skipping..." << std::endl;
|
||||
} else if (resolvedPathExtension == ".zim" || resolvedPathExtension == ".zimaa") {
|
||||
if (!this->addBookFromPath(pathString, pathString, "", false)) {
|
||||
std::cerr << "Could not add " << pathString << " into the library." << std::endl;
|
||||
} else if (verboseFlag) {
|
||||
std::cout << "Added " << pathString << " into the library." << std::endl;
|
||||
totalBooksAdded++;
|
||||
}
|
||||
} else if (verboseFlag) {
|
||||
std::cout << "Skipped " << pathString << " - unsupported file type or permission denied." << std::endl;
|
||||
}
|
||||
}
|
||||
iteratedDirs.insert(currentPath);
|
||||
}
|
||||
|
||||
if (verboseFlag)
|
||||
std::cout << "Traversal completed. Total books added: " << totalBooksAdded << std::endl;
|
||||
}
|
||||
|
||||
bool Manager::readBookFromPath(const std::string& path, kiwix::Book* book)
|
||||
{
|
||||
std::string tmp_path = path;
|
||||
|
||||
@@ -31,6 +31,7 @@ kiwix_sources = [
|
||||
'server/internalServer_catalog.cpp',
|
||||
'server/i18n.cpp',
|
||||
'opds_catalog.cpp',
|
||||
'spelling_correction.cpp',
|
||||
'version.cpp'
|
||||
]
|
||||
kiwix_sources += lib_resources
|
||||
|
||||
@@ -24,33 +24,37 @@
|
||||
|
||||
namespace kiwix {
|
||||
|
||||
HumanReadableNameMapper::HumanReadableNameMapper(kiwix::Library& library, bool withAlias) {
|
||||
for (auto& bookId: library.filter(kiwix::Filter().local(true).valid(true))) {
|
||||
HumanReadableNameMapper::HumanReadableNameMapper(const kiwix::Library& library, bool withAlias) {
|
||||
for (auto& bookId: library.filter(kiwix::Filter())) {
|
||||
auto& currentBook = library.getBookById(bookId);
|
||||
auto bookName = currentBook.getHumanReadableIdFromPath();
|
||||
m_idToName[bookId] = bookName;
|
||||
m_nameToId[bookName] = bookId;
|
||||
mapName(library, bookName, bookId);
|
||||
|
||||
if (!withAlias)
|
||||
continue;
|
||||
|
||||
auto aliasName = replaceRegex(bookName, "", "_[[:digit:]]{4}-[[:digit:]]{2}$");
|
||||
if (aliasName == bookName) {
|
||||
continue;
|
||||
}
|
||||
if (m_nameToId.find(aliasName) == m_nameToId.end()) {
|
||||
m_nameToId[aliasName] = bookId;
|
||||
} else {
|
||||
auto alreadyPresentPath = library.getBookById(m_nameToId[aliasName]).getPath();
|
||||
std::cerr << "Path collision: " << alreadyPresentPath
|
||||
<< " and " << currentBook.getPath()
|
||||
<< " can't share the same URL path '" << aliasName << "'."
|
||||
<< " Therefore, only " << alreadyPresentPath
|
||||
<< " will be served." << std::endl;
|
||||
if (aliasName != bookName) {
|
||||
mapName(library, aliasName, bookId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void HumanReadableNameMapper::mapName(const Library& library, std::string name, std::string bookId) {
|
||||
if (m_nameToId.find(name) == m_nameToId.end()) {
|
||||
m_nameToId[name] = bookId;
|
||||
} else {
|
||||
const auto& currentBook = library.getBookById(bookId);
|
||||
auto alreadyPresentPath = library.getBookById(m_nameToId[name]).getPath();
|
||||
std::cerr << "Path collision: '" << alreadyPresentPath
|
||||
<< "' and '" << currentBook.getPath()
|
||||
<< "' can't share the same URL path '" << name << "'."
|
||||
<< " Therefore, only '" << alreadyPresentPath
|
||||
<< "' will be served." << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
std::string HumanReadableNameMapper::getNameForId(const std::string& id) const {
|
||||
return m_idToName.at(id);
|
||||
}
|
||||
|
||||
@@ -51,24 +51,26 @@ typedef kainjow::mustache::list IllustrationInfo;
|
||||
IllustrationInfo getBookIllustrationInfo(const Book& book)
|
||||
{
|
||||
kainjow::mustache::list illustrations;
|
||||
if ( book.isPathValid() ) {
|
||||
for ( const auto& illustration : book.getIllustrations() ) {
|
||||
// For now, we are handling only sizexsize@1 illustration.
|
||||
// So we can simply pass one size to mustache.
|
||||
illustrations.push_back(kainjow::mustache::object{
|
||||
{"icon_size", to_string(illustration->width)},
|
||||
{"icon_mimetype", illustration->mimeType}
|
||||
});
|
||||
}
|
||||
for ( const auto& illustration : book.getIllustrations() ) {
|
||||
// For now, we are handling only sizexsize@1 illustration.
|
||||
// So we can simply pass one size to mustache.
|
||||
illustrations.push_back(kainjow::mustache::object{
|
||||
{"icon_size", to_string(illustration->width)},
|
||||
{"icon_mimetype", illustration->mimeType}
|
||||
});
|
||||
}
|
||||
return illustrations;
|
||||
}
|
||||
|
||||
std::string fullEntryXML(const Book& book, const std::string& rootLocation, const std::string& contentId)
|
||||
std::string fullEntryXML(const Book& book,
|
||||
const std::string& rootLocation,
|
||||
const std::string& contentAccessUrl,
|
||||
const std::string& contentId)
|
||||
{
|
||||
const auto bookDate = book.getDate() + "T00:00:00Z";
|
||||
const kainjow::mustache::object data{
|
||||
{"root", rootLocation},
|
||||
{"contentAccessUrl", onlyAsNonEmptyMustacheValue(contentAccessUrl)},
|
||||
{"id", book.getId()},
|
||||
{"name", book.getName()},
|
||||
{"title", book.getTitle()},
|
||||
@@ -105,7 +107,12 @@ std::string partialEntryXML(const Book& book, const std::string& rootLocation)
|
||||
return render_template(xmlTemplate, data);
|
||||
}
|
||||
|
||||
BooksData getBooksData(const Library* library, const NameMapper* nameMapper, const std::vector<std::string>& bookIds, const std::string& rootLocation, bool partial)
|
||||
BooksData getBooksData(const Library* library,
|
||||
const NameMapper* nameMapper,
|
||||
const std::vector<std::string>& bookIds,
|
||||
const std::string& rootLocation,
|
||||
const std::string& contentAccessUrl,
|
||||
bool partial)
|
||||
{
|
||||
BooksData booksData;
|
||||
for ( const auto& bookId : bookIds ) {
|
||||
@@ -114,7 +121,7 @@ BooksData getBooksData(const Library* library, const NameMapper* nameMapper, con
|
||||
const std::string contentId = nameMapper->getNameForId(bookId);
|
||||
const auto entryXML = partial
|
||||
? partialEntryXML(book, rootLocation)
|
||||
: fullEntryXML(book, rootLocation, contentId);
|
||||
: fullEntryXML(book, rootLocation, contentAccessUrl, contentId);
|
||||
booksData.push_back(kainjow::mustache::object{ {"entry", entryXML} });
|
||||
} catch ( const std::out_of_range& ) {
|
||||
// the book was removed from the library since its id was obtained
|
||||
@@ -129,7 +136,7 @@ BooksData getBooksData(const Library* library, const NameMapper* nameMapper, con
|
||||
|
||||
string OPDSDumper::dumpOPDSFeed(const std::vector<std::string>& bookIds, const std::string& query) const
|
||||
{
|
||||
const auto booksData = getBooksData(library, nameMapper, bookIds, rootLocation, false);
|
||||
const auto booksData = getBooksData(library, nameMapper, bookIds, rootLocation, contentAccessUrl, false);
|
||||
const kainjow::mustache::object template_data{
|
||||
{"date", gen_date_str()},
|
||||
{"root", rootLocation},
|
||||
@@ -147,7 +154,7 @@ string OPDSDumper::dumpOPDSFeed(const std::vector<std::string>& bookIds, const s
|
||||
string OPDSDumper::dumpOPDSFeedV2(const std::vector<std::string>& bookIds, const std::string& query, bool partial) const
|
||||
{
|
||||
const auto endpointRoot = rootLocation + "/catalog/v2";
|
||||
const auto booksData = getBooksData(library, nameMapper, bookIds, rootLocation, partial);
|
||||
const auto booksData = getBooksData(library, nameMapper, bookIds, rootLocation, contentAccessUrl, partial);
|
||||
|
||||
const char* const endpoint = partial ? "/partial_entries" : "/entries";
|
||||
const std::string url = endpoint + (query.empty() ? "" : "?" + query);
|
||||
@@ -172,7 +179,7 @@ std::string OPDSDumper::dumpOPDSCompleteEntry(const std::string& bookId) const
|
||||
const std::string contentId = nameMapper->getNameForId(bookId);
|
||||
return XML_HEADER
|
||||
+ "\n"
|
||||
+ fullEntryXML(book, rootLocation, contentId);
|
||||
+ fullEntryXML(book, rootLocation, contentAccessUrl, contentId);
|
||||
}
|
||||
|
||||
std::string OPDSDumper::categoriesOPDSFeed() const
|
||||
|
||||
@@ -32,9 +32,42 @@
|
||||
#include "libkiwix-resources.h"
|
||||
#include "tools/stringTools.h"
|
||||
|
||||
#include "server/i18n_utils.h"
|
||||
|
||||
namespace kiwix
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
ParameterizedMessage searchResultsPageTitleMsg(const std::string& searchPattern)
|
||||
{
|
||||
return ParameterizedMessage("search-results-page-title",
|
||||
{{"SEARCH_PATTERN", searchPattern}}
|
||||
);
|
||||
}
|
||||
|
||||
ParameterizedMessage searchResultsPageHeaderMsg(const std::string& searchPattern,
|
||||
const kainjow::mustache::data& r)
|
||||
{
|
||||
if ( r.get("count")->string_value() == "0" ) {
|
||||
return ParameterizedMessage("empty-search-results-page-header",
|
||||
{{"SEARCH_PATTERN", searchPattern}}
|
||||
);
|
||||
} else {
|
||||
return ParameterizedMessage("search-results-page-header",
|
||||
{
|
||||
{"SEARCH_PATTERN", searchPattern},
|
||||
{"START", r.get("startLabel")->string_value()},
|
||||
{"END", r.get("end") ->string_value()},
|
||||
{"COUNT", r.get("count")->string_value()},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
/* Constructor */
|
||||
SearchRenderer::SearchRenderer(zim::SearchResultSet srs,
|
||||
unsigned int start, unsigned int estimatedResultCount)
|
||||
@@ -170,10 +203,20 @@ std::string SearchRenderer::renderTemplate(const std::string& tmpl_str, const Na
|
||||
result.set("absolutePath", absPathPrefix + urlEncode(path));
|
||||
result.set("snippet", it.getSnippet());
|
||||
if (library) {
|
||||
result.set("bookTitle", library->getBookById(zim_id).getTitle());
|
||||
const std::string bookTitle = library->getBookById(zim_id).getTitle();
|
||||
const ParameterizedMessage bookInfoMsg("search-result-book-info",
|
||||
{{"BOOK_TITLE", bookTitle}}
|
||||
);
|
||||
result.set("bookInfo", bookInfoMsg.getText(userlang)); // for HTML
|
||||
result.set("bookTitle", bookTitle); // for XML
|
||||
}
|
||||
if (it.getWordCount() >= 0) {
|
||||
result.set("wordCount", kiwix::beautifyInteger(it.getWordCount()));
|
||||
const auto wordCountStr = kiwix::beautifyInteger(it.getWordCount());
|
||||
const ParameterizedMessage wordCountMsg("word-count",
|
||||
{{"COUNT", wordCountStr}}
|
||||
);
|
||||
result.set("wordCountInfo", wordCountMsg.getText(userlang)); // for HTML
|
||||
result.set("wordCount", wordCountStr); // for XML
|
||||
}
|
||||
|
||||
items.push_back(result);
|
||||
@@ -181,9 +224,9 @@ std::string SearchRenderer::renderTemplate(const std::string& tmpl_str, const Na
|
||||
kainjow::mustache::data results;
|
||||
results.set("items", items);
|
||||
results.set("count", kiwix::beautifyInteger(estimatedResultCount));
|
||||
results.set("hasResults", estimatedResultCount != 0);
|
||||
results.set("start", kiwix::beautifyInteger(resultStart));
|
||||
results.set("end", kiwix::beautifyInteger(std::min(resultStart+pageLength-1, estimatedResultCount)));
|
||||
results.set("startLabel", kiwix::beautifyInteger(resultStart+1));
|
||||
results.set("end", kiwix::beautifyInteger(std::min(resultStart+pageLength, estimatedResultCount)));
|
||||
|
||||
// pagination
|
||||
auto pagination = buildPagination(
|
||||
@@ -198,12 +241,15 @@ std::string SearchRenderer::renderTemplate(const std::string& tmpl_str, const Na
|
||||
searchBookQuery
|
||||
);
|
||||
|
||||
|
||||
kainjow::mustache::data allData;
|
||||
allData.set("searchProtocolPrefix", searchProtocolPrefix);
|
||||
allData.set("results", results);
|
||||
allData.set("pagination", pagination);
|
||||
allData.set("query", query);
|
||||
const auto pageHeaderMsg = searchResultsPageHeaderMsg(searchPattern, results);
|
||||
const kainjow::mustache::object allData{
|
||||
{"PAGE_TITLE", searchResultsPageTitleMsg(searchPattern).getText(userlang)},
|
||||
{"PAGE_HEADER", pageHeaderMsg.getText(userlang)},
|
||||
{"searchProtocolPrefix", searchProtocolPrefix},
|
||||
{"results", results},
|
||||
{"pagination", pagination},
|
||||
{"query", query},
|
||||
};
|
||||
|
||||
kainjow::mustache::mustache tmpl(tmpl_str);
|
||||
|
||||
|
||||
@@ -29,6 +29,22 @@
|
||||
|
||||
namespace kiwix {
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
std::string makeServerUrl(std::string host, int port, std::string root)
|
||||
{
|
||||
const int httpDefaultPort = 80;
|
||||
|
||||
if (port == httpDefaultPort) {
|
||||
return "http://" + host + root;
|
||||
} else {
|
||||
return "http://" + host + ":" + std::to_string(port) + root;
|
||||
}
|
||||
}
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
Server::Server(LibraryPtr library, std::shared_ptr<NameMapper> nameMapper) :
|
||||
mp_library(library),
|
||||
mp_nameMapper(nameMapper),
|
||||
@@ -51,9 +67,18 @@ bool Server::start() {
|
||||
m_withTaskbar,
|
||||
m_withLibraryButton,
|
||||
m_blockExternalLinks,
|
||||
m_ipMode,
|
||||
m_indexTemplateString,
|
||||
m_ipConnectionLimit));
|
||||
return mp_server->start();
|
||||
m_ipConnectionLimit,
|
||||
m_catalogOnlyMode,
|
||||
m_contentServerUrl));
|
||||
if (mp_server->start()) {
|
||||
// this syncs m_addr of InternalServer and Server as they may diverge
|
||||
m_addr = mp_server->getAddress();
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void Server::stop() {
|
||||
@@ -66,22 +91,53 @@ void Server::stop() {
|
||||
void Server::setRoot(const std::string& root)
|
||||
{
|
||||
m_root = root;
|
||||
if (m_root[0] != '/') {
|
||||
m_root = "/" + m_root;
|
||||
}
|
||||
if (m_root.back() == '/') {
|
||||
m_root.erase(m_root.size() - 1);
|
||||
while (!m_root.empty() && m_root.back() == '/')
|
||||
m_root.pop_back();
|
||||
|
||||
while (!m_root.empty() && m_root.front() == '/')
|
||||
m_root = m_root.substr(1);
|
||||
m_root = m_root.empty() ? m_root : "/" + m_root;
|
||||
}
|
||||
|
||||
void Server::setAddress(const std::string& addr)
|
||||
{
|
||||
m_addr.addr.clear();
|
||||
m_addr.addr6.clear();
|
||||
|
||||
if (addr.empty()) return;
|
||||
|
||||
if (addr.find(':') != std::string::npos) { // IPv6
|
||||
m_addr.addr6 = (addr[0] == '[') ? addr.substr(1, addr.length() - 2) : addr; // Remove brackets if any
|
||||
} else {
|
||||
m_addr.addr = addr;
|
||||
}
|
||||
}
|
||||
|
||||
int Server::getPort()
|
||||
int Server::getPort() const
|
||||
{
|
||||
return mp_server->getPort();
|
||||
return m_port;
|
||||
}
|
||||
|
||||
std::string Server::getAddress()
|
||||
IpAddress Server::getAddress() const
|
||||
{
|
||||
return mp_server->getAddress();
|
||||
return m_addr;
|
||||
}
|
||||
|
||||
IpMode Server::getIpMode() const
|
||||
{
|
||||
return mp_server->getIpMode();
|
||||
}
|
||||
|
||||
std::vector<std::string> Server::getServerAccessUrls() const
|
||||
{
|
||||
std::vector<std::string> result;
|
||||
if (!m_addr.addr.empty()) {
|
||||
result.push_back(makeServerUrl(m_addr.addr, m_port, m_root));
|
||||
}
|
||||
if (!m_addr.addr6.empty()) {
|
||||
result.push_back(makeServerUrl("[" + m_addr.addr6 + "]", m_port, m_root));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "i18n.h"
|
||||
#include "i18n_utils.h"
|
||||
|
||||
#include "tools/otherTools.h"
|
||||
|
||||
@@ -112,8 +112,12 @@ std::string expandParameterizedString(const std::string& lang,
|
||||
const std::string& key,
|
||||
const Parameters& params)
|
||||
{
|
||||
kainjow::mustache::object mustacheParams;
|
||||
for( const auto& kv : params ) {
|
||||
mustacheParams[kv.first] = kv.second;
|
||||
}
|
||||
const std::string tmpl = getTranslatedString(lang, key);
|
||||
return render_template(tmpl, params);
|
||||
return render_template(tmpl, mustacheParams);
|
||||
}
|
||||
|
||||
} // namespace i18n
|
||||
@@ -189,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
|
||||
|
||||
@@ -17,10 +17,10 @@
|
||||
* MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef KIWIX_SERVER_I18N
|
||||
#define KIWIX_SERVER_I18N
|
||||
#ifndef KIWIX_SERVER_I18N_UTILS
|
||||
#define KIWIX_SERVER_I18N_UTILS
|
||||
|
||||
#include <string>
|
||||
#include "i18n.h"
|
||||
#include <mustache.hpp>
|
||||
|
||||
namespace kiwix
|
||||
@@ -39,36 +39,9 @@ struct I18nStringTable {
|
||||
const char* get(const std::string& key) const;
|
||||
};
|
||||
|
||||
std::string getTranslatedString(const std::string& lang, const std::string& key);
|
||||
|
||||
namespace i18n
|
||||
{
|
||||
|
||||
typedef kainjow::mustache::object Parameters;
|
||||
|
||||
std::string expandParameterizedString(const std::string& lang,
|
||||
const std::string& key,
|
||||
const Parameters& params);
|
||||
|
||||
class GetTranslatedString
|
||||
{
|
||||
public:
|
||||
explicit GetTranslatedString(const std::string& lang) : m_lang(lang) {}
|
||||
|
||||
std::string operator()(const std::string& key) const
|
||||
{
|
||||
return getTranslatedString(m_lang, key);
|
||||
}
|
||||
|
||||
std::string operator()(const std::string& key, const Parameters& params) const
|
||||
{
|
||||
return expandParameterizedString(m_lang, key, params);
|
||||
}
|
||||
|
||||
private:
|
||||
const std::string m_lang;
|
||||
};
|
||||
|
||||
class GetTranslatedStringWithMsgId
|
||||
{
|
||||
typedef kainjow::mustache::basic_data<std::string> MustacheString;
|
||||
@@ -93,24 +66,6 @@ private:
|
||||
|
||||
} // namespace i18n
|
||||
|
||||
struct ParameterizedMessage
|
||||
{
|
||||
public: // types
|
||||
typedef kainjow::mustache::object Parameters;
|
||||
|
||||
public: // functions
|
||||
ParameterizedMessage(const std::string& msgId, const Parameters& params)
|
||||
: msgId(msgId)
|
||||
, params(params)
|
||||
{}
|
||||
|
||||
std::string getText(const std::string& lang) const;
|
||||
|
||||
private: // data
|
||||
const std::string msgId;
|
||||
const Parameters params;
|
||||
};
|
||||
|
||||
struct LangPreference
|
||||
{
|
||||
const std::string lang;
|
||||
@@ -125,4 +80,4 @@ std::string selectMostSuitableLanguage(const UserLangPreferences& prefs);
|
||||
|
||||
} // namespace kiwix
|
||||
|
||||
#endif // KIWIX_SERVER_I18N
|
||||
#endif // KIWIX_SERVER_I18N_UTILS
|
||||
@@ -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,14 +85,18 @@ namespace kiwix {
|
||||
namespace
|
||||
{
|
||||
|
||||
inline std::string normalizeRootUrl(std::string rootUrl)
|
||||
bool ipAvailable(const std::string addr)
|
||||
{
|
||||
while ( !rootUrl.empty() && rootUrl.back() == '/' )
|
||||
rootUrl.pop_back();
|
||||
auto interfaces = kiwix::getNetworkInterfacesIPv4Or6();
|
||||
|
||||
while ( !rootUrl.empty() && rootUrl.front() == '/' )
|
||||
rootUrl = rootUrl.substr(1);
|
||||
return rootUrl.empty() ? rootUrl : "/" + rootUrl;
|
||||
for (const auto& kv : interfaces) {
|
||||
const auto& interfaceIps = kv.second;
|
||||
if ((interfaceIps.addr == addr) || (interfaceIps.addr6 == addr)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string
|
||||
@@ -111,9 +115,12 @@ std::string getSearchComponent(const RequestContext& request)
|
||||
return query.empty() ? query : "?" + query;
|
||||
}
|
||||
|
||||
Filter get_search_filter(const RequestContext& request, const std::string& prefix="")
|
||||
Filter get_search_filter(const RequestContext& request, const std::string& prefix="", bool catalogOnlyMode = false)
|
||||
{
|
||||
auto filter = kiwix::Filter().valid(true).local(true);
|
||||
auto filter = kiwix::Filter();
|
||||
if ( !catalogOnlyMode ) {
|
||||
filter.valid(true).local(true);
|
||||
}
|
||||
try {
|
||||
filter.query(request.get_argument(prefix+"q"));
|
||||
} catch (const std::out_of_range&) {}
|
||||
@@ -190,12 +197,6 @@ ParameterizedMessage tooManyBooksMsg(size_t nbBooks, size_t limit)
|
||||
);
|
||||
}
|
||||
|
||||
ParameterizedMessage nonParameterizedMessage(const std::string& msgId)
|
||||
{
|
||||
const ParameterizedMessage::Parameters noParams;
|
||||
return ParameterizedMessage(msgId, noParams);
|
||||
}
|
||||
|
||||
struct Error : public std::runtime_error {
|
||||
explicit Error(const ParameterizedMessage& message)
|
||||
: std::runtime_error("Error while handling request"),
|
||||
@@ -413,7 +414,7 @@ public:
|
||||
|
||||
InternalServer::InternalServer(LibraryPtr library,
|
||||
std::shared_ptr<NameMapper> nameMapper,
|
||||
std::string addr,
|
||||
IpAddress addr,
|
||||
int port,
|
||||
std::string root,
|
||||
int nbThreads,
|
||||
@@ -422,11 +423,14 @@ InternalServer::InternalServer(LibraryPtr library,
|
||||
bool withTaskbar,
|
||||
bool withLibraryButton,
|
||||
bool blockExternalLinks,
|
||||
IpMode ipMode,
|
||||
std::string indexTemplateString,
|
||||
int ipConnectionLimit) :
|
||||
int ipConnectionLimit,
|
||||
bool catalogOnlyMode,
|
||||
std::string contentServerUrl) :
|
||||
m_addr(addr),
|
||||
m_port(port),
|
||||
m_root(normalizeRootUrl(root)),
|
||||
m_root(root),
|
||||
m_rootPrefixOfDecodedURL(m_root),
|
||||
m_nbThreads(nbThreads),
|
||||
m_multizimSearchLimit(multizimSearchLimit),
|
||||
@@ -434,6 +438,7 @@ InternalServer::InternalServer(LibraryPtr library,
|
||||
m_withTaskbar(withTaskbar),
|
||||
m_withLibraryButton(withLibraryButton),
|
||||
m_blockExternalLinks(blockExternalLinks),
|
||||
m_ipMode(ipMode),
|
||||
m_indexTemplateString(indexTemplateString.empty() ? RESOURCE::templates::index_html : indexTemplateString),
|
||||
m_ipConnectionLimit(ipConnectionLimit),
|
||||
mp_daemon(nullptr),
|
||||
@@ -441,7 +446,9 @@ InternalServer::InternalServer(LibraryPtr library,
|
||||
mp_nameMapper(nameMapper ? nameMapper : std::shared_ptr<NameMapper>(&defaultNameMapper, NoDelete())),
|
||||
searchCache(getEnvVar<int>("KIWIX_SEARCH_CACHE_SIZE", DEFAULT_CACHE_SIZE)),
|
||||
suggestionSearcherCache(getEnvVar<int>("KIWIX_SUGGESTION_SEARCHER_CACHE_SIZE", std::max((unsigned int) (mp_library->getBookCount(true, true)*0.1), 1U))),
|
||||
m_customizedResources(new CustomizedResources)
|
||||
m_customizedResources(new CustomizedResources),
|
||||
m_catalogOnlyMode(catalogOnlyMode),
|
||||
m_contentServerUrl(contentServerUrl)
|
||||
{
|
||||
m_root = urlEncode(m_root);
|
||||
}
|
||||
@@ -457,28 +464,62 @@ bool InternalServer::start() {
|
||||
if (m_verbose.load())
|
||||
flags |= MHD_USE_DEBUG;
|
||||
|
||||
struct sockaddr_in sockAddr;
|
||||
memset(&sockAddr, 0, sizeof(sockAddr));
|
||||
sockAddr.sin_family = AF_INET;
|
||||
sockAddr.sin_port = htons(m_port);
|
||||
if (m_addr.empty()) {
|
||||
if (0 != INADDR_ANY) {
|
||||
sockAddr.sin_addr.s_addr = htonl(INADDR_ANY);
|
||||
}
|
||||
m_addr = kiwix::getBestPublicIp();
|
||||
|
||||
struct sockaddr_in sockAddr4={0};
|
||||
sockAddr4.sin_family = AF_INET;
|
||||
sockAddr4.sin_port = htons(m_port);
|
||||
struct sockaddr_in6 sockAddr6={0};
|
||||
sockAddr6.sin6_family = AF_INET6;
|
||||
sockAddr6.sin6_port = htons(m_port);
|
||||
|
||||
if (m_addr.addr.empty() && m_addr.addr6.empty()) { // No ip address provided
|
||||
if (m_ipMode == IpMode::AUTO) m_ipMode = IpMode::ALL;
|
||||
sockAddr6.sin6_addr = in6addr_any;
|
||||
sockAddr4.sin_addr.s_addr = htonl(INADDR_ANY);
|
||||
IpAddress bestIps = kiwix::getBestPublicIps();
|
||||
if (m_ipMode == IpMode::IPV4 || m_ipMode == IpMode::ALL) m_addr.addr = bestIps.addr;
|
||||
if (m_ipMode == IpMode::IPV6 || m_ipMode == IpMode::ALL) m_addr.addr6 = bestIps.addr6;
|
||||
} else {
|
||||
if (inet_pton(AF_INET, m_addr.c_str(), &(sockAddr.sin_addr.s_addr)) == 0) {
|
||||
std::cerr << "Ip address " << m_addr << " is not a valid ip address" << std::endl;
|
||||
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) {
|
||||
flags|=MHD_USE_DUAL_STACK;
|
||||
} else if (m_ipMode == IpMode::IPV6) {
|
||||
flags|=MHD_USE_IPv6;
|
||||
}
|
||||
|
||||
struct sockaddr* sockaddr = (m_ipMode==IpMode::ALL || m_ipMode==IpMode::IPV6)
|
||||
? (struct sockaddr*)&sockAddr6
|
||||
: (struct sockaddr*)&sockAddr4;
|
||||
|
||||
mp_daemon = MHD_start_daemon(flags,
|
||||
m_port,
|
||||
NULL,
|
||||
NULL,
|
||||
&staticHandlerCallback,
|
||||
this,
|
||||
MHD_OPTION_SOCK_ADDR, &sockAddr,
|
||||
MHD_OPTION_SOCK_ADDR, sockaddr,
|
||||
MHD_OPTION_THREAD_POOL_SIZE, m_nbThreads,
|
||||
MHD_OPTION_PER_IP_CONNECTION_LIMIT, m_ipConnectionLimit,
|
||||
MHD_OPTION_END);
|
||||
@@ -519,6 +560,19 @@ static MHD_Result staticHandlerCallback(void* cls,
|
||||
cont_cls);
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
MHD_Result add_name_value_pair(void *nvp, enum MHD_ValueKind kind,
|
||||
const char *key, const char *value)
|
||||
{
|
||||
auto& nameValuePairs = *reinterpret_cast<RequestContext::NameValuePairs*>(nvp);
|
||||
nameValuePairs.push_back({key, value});
|
||||
return MHD_YES;
|
||||
}
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
MHD_Result InternalServer::handlerCallback(struct MHD_Connection* connection,
|
||||
const char* fullUrl,
|
||||
const char* method,
|
||||
@@ -535,7 +589,10 @@ MHD_Result InternalServer::handlerCallback(struct MHD_Connection* connection,
|
||||
}
|
||||
|
||||
const auto url = fullURL2LocalURL(fullUrl, m_rootPrefixOfDecodedURL);
|
||||
RequestContext request(connection, m_root, url, method, version);
|
||||
RequestContext::NameValuePairs headers, queryArgs;
|
||||
MHD_get_connection_values(connection, MHD_HEADER_KIND, add_name_value_pair, &headers);
|
||||
MHD_get_connection_values(connection, MHD_GET_ARGUMENT_KIND, add_name_value_pair, &queryArgs);
|
||||
RequestContext request(m_root, url, method, version, headers, queryArgs);
|
||||
|
||||
if (m_verbose.load() ) {
|
||||
request.print_debug_info();
|
||||
@@ -564,7 +621,7 @@ MHD_Result InternalServer::handlerCallback(struct MHD_Connection* connection,
|
||||
response->set_etag_body(getLibraryId());
|
||||
}
|
||||
|
||||
auto ret = response->send(request, connection);
|
||||
auto ret = response->send(request, m_verbose.load(), connection);
|
||||
auto end_time = std::chrono::steady_clock::now();
|
||||
auto time_span = std::chrono::duration_cast<std::chrono::duration<double>>(end_time - start_time);
|
||||
if (m_verbose.load()) {
|
||||
@@ -593,20 +650,19 @@ std::unique_ptr<Response> InternalServer::handle_request(const RequestContext& r
|
||||
{
|
||||
try {
|
||||
if (! request.is_valid_url()) {
|
||||
return HTTP404Response(*this, request)
|
||||
+ urlNotFoundMsg;
|
||||
return UrlNotFoundResponse(request);
|
||||
}
|
||||
|
||||
if ( request.get_url() == "" ) {
|
||||
// Redirect /ROOT_LOCATION to /ROOT_LOCATION/ (note the added slash)
|
||||
// so that relative URLs are resolved correctly
|
||||
const std::string query = getSearchComponent(request);
|
||||
return Response::build_redirect(*this, m_root + "/" + query);
|
||||
return Response::build_redirect(m_root + "/" + query);
|
||||
}
|
||||
|
||||
const ETag etag = get_matching_if_none_match_etag(request, getLibraryId());
|
||||
if ( etag )
|
||||
return Response::build_304(*this, etag);
|
||||
return Response::build_304(etag);
|
||||
|
||||
const auto url = request.get_url();
|
||||
if ( isLocallyCustomizedResource(url) )
|
||||
@@ -647,15 +703,13 @@ std::unique_ptr<Response> InternalServer::handle_request(const RequestContext& r
|
||||
|
||||
const std::string contentUrl = m_root + "/content" + urlEncode(url);
|
||||
const std::string query = getSearchComponent(request);
|
||||
return Response::build_redirect(*this, contentUrl + query);
|
||||
return Response::build_redirect(contentUrl + query);
|
||||
} catch (std::exception& e) {
|
||||
fprintf(stderr, "===== Unhandled error : %s\n", e.what());
|
||||
return HTTP500Response(*this, request)
|
||||
+ e.what();
|
||||
return HTTP500Response(request, m_root, request.get_full_url(), e.what());
|
||||
} catch (...) {
|
||||
fprintf(stderr, "===== Unhandled unknown error\n");
|
||||
return HTTP500Response(*this, request)
|
||||
+ "Unknown error";
|
||||
return HTTP500Response(request, m_root, request.get_full_url());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -668,7 +722,7 @@ MustacheData InternalServer::get_default_data() const
|
||||
|
||||
std::unique_ptr<Response> InternalServer::build_homepage(const RequestContext& request)
|
||||
{
|
||||
return ContentResponse::build(*this, m_indexTemplateString, get_default_data(), "text/html; charset=utf-8");
|
||||
return ContentResponse::build(m_indexTemplateString, get_default_data(), "text/html; charset=utf-8");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -697,8 +751,7 @@ std::unique_ptr<Response> InternalServer::handle_suggest(const RequestContext& r
|
||||
}
|
||||
|
||||
if ( startsWith(request.get_url(), "/suggest/") ) {
|
||||
return HTTP404Response(*this, request)
|
||||
+ urlNotFoundMsg;
|
||||
return UrlNotFoundResponse(request);
|
||||
}
|
||||
|
||||
std::string bookName, bookId;
|
||||
@@ -712,7 +765,7 @@ std::unique_ptr<Response> InternalServer::handle_suggest(const RequestContext& r
|
||||
}
|
||||
|
||||
if (archive == nullptr) {
|
||||
return HTTP404Response(*this, request)
|
||||
return HTTP404Response(request)
|
||||
+ noSuchBookErrorMsg(bookName);
|
||||
}
|
||||
|
||||
@@ -747,7 +800,7 @@ std::unique_ptr<Response> InternalServer::handle_suggest(const RequestContext& r
|
||||
results.addFTSearchSuggestion(request.get_user_language(), queryString);
|
||||
}
|
||||
|
||||
return ContentResponse::build(*this, results.getJSON(), "application/json; charset=utf-8");
|
||||
return ContentResponse::build(results.getJSON(), "application/json; charset=utf-8");
|
||||
}
|
||||
|
||||
std::unique_ptr<Response> InternalServer::handle_viewer_settings(const RequestContext& request)
|
||||
@@ -759,10 +812,9 @@ std::unique_ptr<Response> InternalServer::handle_viewer_settings(const RequestCo
|
||||
const kainjow::mustache::object data{
|
||||
{"enable_toolbar", m_withTaskbar ? "true" : "false" },
|
||||
{"enable_link_blocking", m_blockExternalLinks ? "true" : "false" },
|
||||
{"enable_library_button", m_withLibraryButton ? "true" : "false" },
|
||||
{"default_user_language", request.get_user_language() }
|
||||
{"enable_library_button", m_withLibraryButton ? "true" : "false" }
|
||||
};
|
||||
return ContentResponse::build(*this, RESOURCE::templates::viewer_settings_js, data, "application/javascript; charset=utf-8");
|
||||
return ContentResponse::build(RESOURCE::templates::viewer_settings_js, data, "application/javascript; charset=utf-8");
|
||||
}
|
||||
|
||||
std::string InternalServer::getNoJSDownloadPageHTML(const std::string& bookId, const std::string& userLang) const
|
||||
@@ -788,6 +840,15 @@ std::string InternalServer::getNoJSDownloadPageHTML(const std::string& bookId, c
|
||||
);
|
||||
}
|
||||
|
||||
void InternalServer::setContentAccessUrl(LibraryDumper& libDumper) const
|
||||
{
|
||||
if ( !m_contentServerUrl.empty() ) {
|
||||
libDumper.setContentAccessUrl(m_contentServerUrl + "/content");
|
||||
} else if ( !m_catalogOnlyMode ) {
|
||||
libDumper.setContentAccessUrl(m_root + "/content");
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<Response> InternalServer::handle_no_js(const RequestContext& request)
|
||||
{
|
||||
const auto url = request.get_url();
|
||||
@@ -795,12 +856,13 @@ std::unique_ptr<Response> InternalServer::handle_no_js(const RequestContext& req
|
||||
HTMLDumper htmlDumper(mp_library.get(), mp_nameMapper.get());
|
||||
htmlDumper.setRootLocation(m_root);
|
||||
htmlDumper.setLibraryId(getLibraryId());
|
||||
setContentAccessUrl(htmlDumper);
|
||||
auto userLang = request.get_user_language();
|
||||
htmlDumper.setUserLanguage(userLang);
|
||||
std::string content;
|
||||
|
||||
if (urlParts.size() == 1) {
|
||||
auto filter = get_search_filter(request);
|
||||
auto filter = get_search_filter(request, "", m_catalogOnlyMode);
|
||||
try {
|
||||
if (request.get_argument("category") == "") {
|
||||
filter.clearCategory();
|
||||
@@ -817,19 +879,13 @@ std::unique_ptr<Response> InternalServer::handle_no_js(const RequestContext& req
|
||||
const auto bookId = mp_nameMapper->getIdForName(urlParts[2]);
|
||||
content = getNoJSDownloadPageHTML(bookId, userLang);
|
||||
} catch (const std::out_of_range&) {
|
||||
return HTTP404Response(*this, request)
|
||||
+ urlNotFoundMsg;
|
||||
return UrlNotFoundResponse(request);
|
||||
}
|
||||
} else {
|
||||
return HTTP404Response(*this, request)
|
||||
+ urlNotFoundMsg;
|
||||
return UrlNotFoundResponse(request);
|
||||
}
|
||||
|
||||
return ContentResponse::build(
|
||||
*this,
|
||||
content,
|
||||
"text/html; charset=utf-8"
|
||||
);
|
||||
return ContentResponse::build(content, "text/html; charset=utf-8");
|
||||
}
|
||||
|
||||
namespace
|
||||
@@ -867,14 +923,12 @@ std::unique_ptr<Response> InternalServer::handle_skin(const RequestContext& requ
|
||||
try {
|
||||
const auto accessType = staticResourceAccessType(request, resourceCacheId);
|
||||
auto response = ContentResponse::build(
|
||||
*this,
|
||||
getResource(resourceName),
|
||||
getMimeTypeForFile(resourceName));
|
||||
response->set_kind(accessType);
|
||||
return std::move(response);
|
||||
} catch (const ResourceNotFound& e) {
|
||||
return HTTP404Response(*this, request)
|
||||
+ urlNotFoundMsg;
|
||||
return UrlNotFoundResponse(request);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -887,20 +941,17 @@ std::unique_ptr<Response> InternalServer::handle_search(const RequestContext& re
|
||||
if ( startsWith(request.get_url(), "/search/") ) {
|
||||
if (request.get_url() == "/search/searchdescription.xml") {
|
||||
return ContentResponse::build(
|
||||
*this,
|
||||
RESOURCE::ft_opensearchdescription_xml,
|
||||
get_default_data(),
|
||||
"application/opensearchdescription+xml");
|
||||
}
|
||||
return HTTP404Response(*this, request)
|
||||
+ urlNotFoundMsg;
|
||||
return UrlNotFoundResponse(request);
|
||||
}
|
||||
|
||||
try {
|
||||
return handle_search_request(request);
|
||||
} catch (const Error& e) {
|
||||
return HTTP400Response(*this, request)
|
||||
+ invalidUrlMsg
|
||||
return HTTP400Response(request)
|
||||
+ e.message();
|
||||
}
|
||||
}
|
||||
@@ -941,11 +992,12 @@ 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);
|
||||
HTTPErrorResponse response(*this, request, MHD_HTTP_NOT_FOUND,
|
||||
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",
|
||||
cssUrl);
|
||||
cssUrl,
|
||||
/*includeKiwixResponseData=*/true);
|
||||
response += nonParameterizedMessage("no-search-results");
|
||||
// XXX: Now this has to be handled by the iframe-based viewer which
|
||||
// XXX: has to resolve if the book selection resulted in a single book.
|
||||
@@ -959,26 +1011,25 @@ std::unique_ptr<Response> InternalServer::handle_search_request(const RequestCon
|
||||
return response;
|
||||
}
|
||||
|
||||
const auto start = max(1u, request.get_optional_param("start", 1u));
|
||||
const auto start = max(0u, request.get_optional_param("start", 0u));
|
||||
const auto pageLength = getSearchPageSize(request);
|
||||
|
||||
/* Get the results */
|
||||
SearchRenderer renderer(search->getResults(start-1, pageLength), start,
|
||||
SearchRenderer renderer(search->getResults(start, pageLength), start,
|
||||
search->getEstimatedMatches());
|
||||
renderer.setSearchPattern(searchInfo.pattern);
|
||||
renderer.setSearchBookQuery(searchInfo.bookFilterQuery);
|
||||
renderer.setProtocolPrefix(m_root + "/content/");
|
||||
renderer.setSearchProtocolPrefix(m_root + "/search");
|
||||
renderer.setPageLength(pageLength);
|
||||
renderer.setUserLang(request.get_user_language());
|
||||
if (request.get_requested_format() == "xml") {
|
||||
return ContentResponse::build(
|
||||
*this,
|
||||
renderer.getXml(*mp_nameMapper, mp_library.get()),
|
||||
"application/rss+xml; charset=utf-8"
|
||||
);
|
||||
}
|
||||
auto response = ContentResponse::build(
|
||||
*this,
|
||||
renderer.getHtml(*mp_nameMapper, mp_library.get()),
|
||||
"text/html; charset=utf-8"
|
||||
);
|
||||
@@ -1001,8 +1052,7 @@ std::unique_ptr<Response> InternalServer::handle_random(const RequestContext& re
|
||||
}
|
||||
|
||||
if ( startsWith(request.get_url(), "/random/") ) {
|
||||
return HTTP404Response(*this, request)
|
||||
+ urlNotFoundMsg;
|
||||
return UrlNotFoundResponse(request);
|
||||
}
|
||||
|
||||
std::string bookName;
|
||||
@@ -1016,7 +1066,7 @@ std::unique_ptr<Response> InternalServer::handle_random(const RequestContext& re
|
||||
}
|
||||
|
||||
if (archive == nullptr) {
|
||||
return HTTP404Response(*this, request)
|
||||
return HTTP404Response(request)
|
||||
+ noSuchBookErrorMsg(bookName);
|
||||
}
|
||||
|
||||
@@ -1024,7 +1074,7 @@ std::unique_ptr<Response> InternalServer::handle_random(const RequestContext& re
|
||||
auto entry = archive->getRandomEntry();
|
||||
return build_redirect(bookName, getFinalItem(*archive, entry));
|
||||
} catch(zim::EntryNotFound& e) {
|
||||
return HTTP404Response(*this, request)
|
||||
return HTTP404Response(request)
|
||||
+ nonParameterizedMessage("random-article-failure");
|
||||
}
|
||||
}
|
||||
@@ -1037,13 +1087,10 @@ std::unique_ptr<Response> InternalServer::handle_captured_external(const Request
|
||||
} catch (const std::out_of_range& e) {}
|
||||
|
||||
if (source.empty()) {
|
||||
return HTTP404Response(*this, request)
|
||||
+ urlNotFoundMsg;
|
||||
return UrlNotFoundResponse(request);
|
||||
}
|
||||
|
||||
auto data = get_default_data();
|
||||
data.set("source", source);
|
||||
return ContentResponse::build(*this, 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)
|
||||
@@ -1056,15 +1103,14 @@ std::unique_ptr<Response> InternalServer::handle_catch(const RequestContext& req
|
||||
return handle_captured_external(request);
|
||||
}
|
||||
|
||||
return HTTP404Response(*this, request)
|
||||
+ urlNotFoundMsg;
|
||||
return UrlNotFoundResponse(request);
|
||||
}
|
||||
|
||||
std::vector<std::string>
|
||||
InternalServer::search_catalog(const RequestContext& request,
|
||||
kiwix::OPDSDumper& opdsDumper)
|
||||
{
|
||||
const auto filter = get_search_filter(request);
|
||||
const auto filter = get_search_filter(request, "", m_catalogOnlyMode);
|
||||
std::vector<std::string> bookIdsToDump = mp_library->filter(filter);
|
||||
const auto totalResults = bookIdsToDump.size();
|
||||
const long count = request.get_optional_param("count", 10L);
|
||||
@@ -1078,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
|
||||
@@ -1104,6 +1141,7 @@ const std::string CONTENT_CSP_HEADER =
|
||||
"allow-same-origin "
|
||||
"allow-modals "
|
||||
"allow-popups "
|
||||
"allow-popups-to-escape-sandbox "
|
||||
"allow-forms "
|
||||
"allow-downloads;";
|
||||
|
||||
@@ -1117,7 +1155,7 @@ InternalServer::build_redirect(const std::string& bookName, const zim::Item& ite
|
||||
{
|
||||
const auto contentPath = "/content/" + bookName + "/" + item.getPath();
|
||||
const auto url = m_root + kiwix::urlEncode(contentPath);
|
||||
return Response::build_redirect(*this, url);
|
||||
return Response::build_redirect(url);
|
||||
}
|
||||
|
||||
std::unique_ptr<Response> InternalServer::handle_content(const RequestContext& request)
|
||||
@@ -1140,16 +1178,13 @@ 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 HTTP404Response(*this, request)
|
||||
+ urlNotFoundMsg
|
||||
+ suggestSearchMsg(searchURL, kiwix::urlDecode(pattern));
|
||||
return NewHTTP404Response(request, m_root, m_root + url);
|
||||
}
|
||||
|
||||
const std::string archiveUuid(archive->getUuid());
|
||||
const ETag etag = get_matching_if_none_match_etag(request, archiveUuid);
|
||||
if ( etag )
|
||||
return Response::build_304(*this, etag);
|
||||
return Response::build_304(etag);
|
||||
|
||||
auto urlStr = url.substr(prefixLength + bookName.size());
|
||||
if (urlStr[0] == '/') {
|
||||
@@ -1168,7 +1203,7 @@ std::unique_ptr<Response> InternalServer::handle_content(const RequestContext& r
|
||||
// '-' namespaces, in which case that resource is returned instead.
|
||||
return build_redirect(bookName, getFinalItem(*archive, entry));
|
||||
}
|
||||
auto response = ItemResponse::build(*this, request, entry.getItem());
|
||||
auto response = ItemResponse::build(request, entry.getItem());
|
||||
response->set_etag_body(archiveUuid);
|
||||
|
||||
if ( !startsWith(entry.getItem().getMimetype(), "application/pdf") ) {
|
||||
@@ -1188,10 +1223,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 HTTP404Response(*this, request)
|
||||
+ urlNotFoundMsg
|
||||
+ suggestSearchMsg(searchURL, kiwix::urlDecode(pattern));
|
||||
return NewHTTP404Response(request, m_root, m_root + url);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1208,13 +1240,11 @@ std::unique_ptr<Response> InternalServer::handle_raw(const RequestContext& reque
|
||||
bookName = request.get_url_part(1);
|
||||
kind = request.get_url_part(2);
|
||||
} catch (const std::out_of_range& e) {
|
||||
return HTTP404Response(*this, request)
|
||||
+ urlNotFoundMsg;
|
||||
return UrlNotFoundResponse(request);
|
||||
}
|
||||
|
||||
if (kind != "meta" && kind!= "content") {
|
||||
return HTTP404Response(*this, request)
|
||||
+ urlNotFoundMsg
|
||||
return UrlNotFoundResponse(request)
|
||||
+ invalidRawAccessMsg(kind);
|
||||
}
|
||||
|
||||
@@ -1225,15 +1255,14 @@ std::unique_ptr<Response> InternalServer::handle_raw(const RequestContext& reque
|
||||
} catch (const std::out_of_range& e) {}
|
||||
|
||||
if (archive == nullptr) {
|
||||
return HTTP404Response(*this, request)
|
||||
+ urlNotFoundMsg
|
||||
return UrlNotFoundResponse(request)
|
||||
+ noSuchBookErrorMsg(bookName);
|
||||
}
|
||||
|
||||
const std::string archiveUuid(archive->getUuid());
|
||||
const ETag etag = get_matching_if_none_match_etag(request, archiveUuid);
|
||||
if ( etag )
|
||||
return Response::build_304(*this, etag);
|
||||
return Response::build_304(etag);
|
||||
|
||||
// Remove the beggining of the path:
|
||||
// /raw/<bookName>/<kind>/foo
|
||||
@@ -1244,7 +1273,7 @@ std::unique_ptr<Response> InternalServer::handle_raw(const RequestContext& reque
|
||||
try {
|
||||
if (kind == "meta") {
|
||||
auto item = archive->getMetadataItem(itemPath);
|
||||
auto response = ItemResponse::build(*this, request, item);
|
||||
auto response = ItemResponse::build(request, item);
|
||||
response->set_etag_body(archiveUuid);
|
||||
return response;
|
||||
} else {
|
||||
@@ -1252,7 +1281,7 @@ std::unique_ptr<Response> InternalServer::handle_raw(const RequestContext& reque
|
||||
if (entry.isRedirect()) {
|
||||
return build_redirect(bookName, entry.getItem(true));
|
||||
}
|
||||
auto response = ItemResponse::build(*this, request, entry.getItem());
|
||||
auto response = ItemResponse::build(request, entry.getItem());
|
||||
response->set_etag_body(archiveUuid);
|
||||
return response;
|
||||
}
|
||||
@@ -1260,8 +1289,7 @@ std::unique_ptr<Response> InternalServer::handle_raw(const RequestContext& reque
|
||||
if (m_verbose.load()) {
|
||||
printf("Failed to find %s\n", itemPath.c_str());
|
||||
}
|
||||
return HTTP404Response(*this, request)
|
||||
+ urlNotFoundMsg
|
||||
return UrlNotFoundResponse(request)
|
||||
+ rawEntryNotFoundMsg(kind, itemPath);
|
||||
}
|
||||
}
|
||||
@@ -1286,12 +1314,10 @@ std::unique_ptr<Response> InternalServer::handle_locally_customized_resource(con
|
||||
|
||||
auto byteRange = request.get_range().resolve(resourceData.size());
|
||||
if (byteRange.kind() != ByteRange::RESOLVED_FULL_CONTENT) {
|
||||
return Response::build_416(*this, resourceData.size());
|
||||
return Response::build_416(resourceData.size());
|
||||
}
|
||||
|
||||
return ContentResponse::build(*this,
|
||||
resourceData,
|
||||
crd.mimeType);
|
||||
return ContentResponse::build(resourceData, crd.mimeType);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ extern "C" {
|
||||
|
||||
#include "library.h"
|
||||
#include "name_mapper.h"
|
||||
#include "tools.h"
|
||||
|
||||
#include <zim/search.h>
|
||||
#include <zim/suggestion.h>
|
||||
@@ -89,12 +90,13 @@ class SearchInfo {
|
||||
|
||||
typedef kainjow::mustache::data MustacheData;
|
||||
class OPDSDumper;
|
||||
class LibraryDumper;
|
||||
|
||||
class InternalServer {
|
||||
public:
|
||||
InternalServer(LibraryPtr library,
|
||||
std::shared_ptr<NameMapper> nameMapper,
|
||||
std::string addr,
|
||||
IpAddress addr,
|
||||
int port,
|
||||
std::string root,
|
||||
int nbThreads,
|
||||
@@ -103,8 +105,11 @@ class InternalServer {
|
||||
bool withTaskbar,
|
||||
bool withLibraryButton,
|
||||
bool blockExternalLinks,
|
||||
IpMode ipMode,
|
||||
std::string indexTemplateString,
|
||||
int ipConnectionLimit);
|
||||
int ipConnectionLimit,
|
||||
bool catalogOnlyMode,
|
||||
std::string zimViewerURL);
|
||||
virtual ~InternalServer();
|
||||
|
||||
MHD_Result handlerCallback(struct MHD_Connection* connection,
|
||||
@@ -116,8 +121,9 @@ 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
|
||||
std::unique_ptr<Response> handle_request(const RequestContext& request);
|
||||
@@ -157,6 +163,8 @@ class InternalServer {
|
||||
std::string getLibraryId() const;
|
||||
|
||||
std::string getNoJSDownloadPageHTML(const std::string& bookId, const std::string& userLang) const;
|
||||
OPDSDumper getOPDSDumper() const;
|
||||
void setContentAccessUrl(LibraryDumper& libDumper) const;
|
||||
|
||||
private: // types
|
||||
class LockableSuggestionSearcher;
|
||||
@@ -164,7 +172,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
|
||||
@@ -174,6 +182,7 @@ class InternalServer {
|
||||
bool m_withTaskbar;
|
||||
bool m_withLibraryButton;
|
||||
bool m_blockExternalLinks;
|
||||
IpMode m_ipMode;
|
||||
std::string m_indexTemplateString;
|
||||
int m_ipConnectionLimit;
|
||||
struct MHD_Daemon* mp_daemon;
|
||||
@@ -189,9 +198,8 @@ class InternalServer {
|
||||
class CustomizedResources;
|
||||
std::unique_ptr<CustomizedResources> m_customizedResources;
|
||||
|
||||
friend std::unique_ptr<Response> Response::build(const InternalServer& server);
|
||||
friend std::unique_ptr<ContentResponse> ContentResponse::build(const InternalServer& server, const std::string& content, const std::string& mimetype);
|
||||
friend std::unique_ptr<Response> ItemResponse::build(const InternalServer& server, const RequestContext& request, const zim::Item& item);
|
||||
const bool m_catalogOnlyMode;
|
||||
const std::string m_contentServerUrl;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -51,6 +51,15 @@ const std::string opdsMimeType[] = {
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
OPDSDumper InternalServer::getOPDSDumper() const
|
||||
{
|
||||
kiwix::OPDSDumper opdsDumper(mp_library.get(), mp_nameMapper.get());
|
||||
opdsDumper.setRootLocation(m_root);
|
||||
opdsDumper.setLibraryId(getLibraryId());
|
||||
setContentAccessUrl(opdsDumper);
|
||||
return opdsDumper;
|
||||
}
|
||||
|
||||
std::unique_ptr<Response> InternalServer::handle_catalog(const RequestContext& request)
|
||||
{
|
||||
if (m_verbose.load()) {
|
||||
@@ -63,8 +72,7 @@ std::unique_ptr<Response> InternalServer::handle_catalog(const RequestContext& r
|
||||
host = request.get_header("Host");
|
||||
url = request.get_url_part(1);
|
||||
} catch (const std::out_of_range&) {
|
||||
return HTTP404Response(*this, request)
|
||||
+ urlNotFoundMsg;
|
||||
return UrlNotFoundResponse(request);
|
||||
}
|
||||
|
||||
if (url == "v2") {
|
||||
@@ -72,19 +80,16 @@ std::unique_ptr<Response> InternalServer::handle_catalog(const RequestContext& r
|
||||
}
|
||||
|
||||
if (url != "searchdescription.xml" && url != "root.xml" && url != "search") {
|
||||
return HTTP404Response(*this, request)
|
||||
+ urlNotFoundMsg;
|
||||
return UrlNotFoundResponse(request);
|
||||
}
|
||||
|
||||
if (url == "searchdescription.xml") {
|
||||
auto response = ContentResponse::build(*this, RESOURCE::opensearchdescription_xml, get_default_data(), "application/opensearchdescription+xml");
|
||||
auto response = ContentResponse::build(RESOURCE::opensearchdescription_xml, get_default_data(), "application/opensearchdescription+xml");
|
||||
return std::move(response);
|
||||
}
|
||||
|
||||
zim::Uuid uuid;
|
||||
kiwix::OPDSDumper opdsDumper(mp_library.get(), mp_nameMapper.get());
|
||||
opdsDumper.setRootLocation(m_root);
|
||||
opdsDumper.setLibraryId(getLibraryId());
|
||||
kiwix::OPDSDumper opdsDumper = getOPDSDumper();
|
||||
std::vector<std::string> bookIdsToDump;
|
||||
if (url == "root.xml") {
|
||||
uuid = zim::Uuid::generate(host);
|
||||
@@ -95,7 +100,6 @@ std::unique_ptr<Response> InternalServer::handle_catalog(const RequestContext& r
|
||||
}
|
||||
|
||||
auto response = ContentResponse::build(
|
||||
*this,
|
||||
opdsDumper.dumpOPDSFeed(bookIdsToDump, request.get_query()),
|
||||
opdsMimeType[OPDS_ACQUISITION_FEED]);
|
||||
return std::move(response);
|
||||
@@ -111,15 +115,14 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2(const RequestContext
|
||||
try {
|
||||
url = request.get_url_part(2);
|
||||
} catch (const std::out_of_range&) {
|
||||
return HTTP404Response(*this, request)
|
||||
+ urlNotFoundMsg;
|
||||
return UrlNotFoundResponse(request);
|
||||
}
|
||||
|
||||
if (url == "root.xml") {
|
||||
return handle_catalog_v2_root(request);
|
||||
} else if (url == "searchdescription.xml") {
|
||||
const std::string endpoint_root = m_root + "/catalog/v2";
|
||||
return ContentResponse::build(*this,
|
||||
return ContentResponse::build(
|
||||
RESOURCE::catalog_v2_searchdescription_xml,
|
||||
kainjow::mustache::object({{"endpoint_root", endpoint_root}}),
|
||||
"application/opensearchdescription+xml"
|
||||
@@ -138,8 +141,7 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2(const RequestContext
|
||||
} else if (url == "illustration") {
|
||||
return handle_catalog_v2_illustration(request);
|
||||
} else {
|
||||
return HTTP404Response(*this, request)
|
||||
+ urlNotFoundMsg;
|
||||
return UrlNotFoundResponse(request);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,7 +149,6 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_root(const RequestCo
|
||||
{
|
||||
const std::string libraryId = getLibraryId();
|
||||
return ContentResponse::build(
|
||||
*this,
|
||||
RESOURCE::templates::catalog_v2_root_xml,
|
||||
kainjow::mustache::object{
|
||||
{"date", gen_date_str()},
|
||||
@@ -164,13 +165,10 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_root(const RequestCo
|
||||
|
||||
std::unique_ptr<Response> InternalServer::handle_catalog_v2_entries(const RequestContext& request, bool partial)
|
||||
{
|
||||
OPDSDumper opdsDumper(mp_library.get(), mp_nameMapper.get());
|
||||
opdsDumper.setRootLocation(m_root);
|
||||
opdsDumper.setLibraryId(getLibraryId());
|
||||
kiwix::OPDSDumper opdsDumper = getOPDSDumper();
|
||||
const auto bookIds = search_catalog(request, opdsDumper);
|
||||
const auto opdsFeed = opdsDumper.dumpOPDSFeedV2(bookIds, request.get_query(), partial);
|
||||
return ContentResponse::build(
|
||||
*this,
|
||||
opdsFeed,
|
||||
opdsMimeType[OPDS_ACQUISITION_FEED]
|
||||
);
|
||||
@@ -181,16 +179,12 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_complete_entry(const
|
||||
try {
|
||||
mp_library->getBookById(entryId);
|
||||
} catch (const std::out_of_range&) {
|
||||
return HTTP404Response(*this, request)
|
||||
+ urlNotFoundMsg;
|
||||
return UrlNotFoundResponse(request);
|
||||
}
|
||||
|
||||
OPDSDumper opdsDumper(mp_library.get(), mp_nameMapper.get());
|
||||
opdsDumper.setRootLocation(m_root);
|
||||
opdsDumper.setLibraryId(getLibraryId());
|
||||
kiwix::OPDSDumper opdsDumper = getOPDSDumper();
|
||||
const auto opdsFeed = opdsDumper.dumpOPDSCompleteEntry(entryId);
|
||||
return ContentResponse::build(
|
||||
*this,
|
||||
opdsFeed,
|
||||
opdsMimeType[OPDS_ENTRY]
|
||||
);
|
||||
@@ -198,11 +192,8 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_complete_entry(const
|
||||
|
||||
std::unique_ptr<Response> InternalServer::handle_catalog_v2_categories(const RequestContext& request)
|
||||
{
|
||||
OPDSDumper opdsDumper(mp_library.get(), mp_nameMapper.get());
|
||||
opdsDumper.setRootLocation(m_root);
|
||||
opdsDumper.setLibraryId(getLibraryId());
|
||||
kiwix::OPDSDumper opdsDumper = getOPDSDumper();
|
||||
return ContentResponse::build(
|
||||
*this,
|
||||
opdsDumper.categoriesOPDSFeed(),
|
||||
opdsMimeType[OPDS_NAVIGATION_FEED]
|
||||
);
|
||||
@@ -210,11 +201,8 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_categories(const Req
|
||||
|
||||
std::unique_ptr<Response> InternalServer::handle_catalog_v2_languages(const RequestContext& request)
|
||||
{
|
||||
OPDSDumper opdsDumper(mp_library.get(), mp_nameMapper.get());
|
||||
opdsDumper.setRootLocation(m_root);
|
||||
opdsDumper.setLibraryId(getLibraryId());
|
||||
kiwix::OPDSDumper opdsDumper = getOPDSDumper();
|
||||
return ContentResponse::build(
|
||||
*this,
|
||||
opdsDumper.languagesOPDSFeed(),
|
||||
opdsMimeType[OPDS_NAVIGATION_FEED]
|
||||
);
|
||||
@@ -228,13 +216,11 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_illustration(const R
|
||||
auto size = request.get_argument<unsigned int>("size");
|
||||
auto illustration = book.getIllustration(size);
|
||||
return ContentResponse::build(
|
||||
*this,
|
||||
illustration->getData(),
|
||||
illustration->mimeType
|
||||
);
|
||||
} catch(...) {
|
||||
return HTTP404Response(*this, request)
|
||||
+ urlNotFoundMsg;
|
||||
return UrlNotFoundResponse(request);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
#include <cctype>
|
||||
|
||||
#include "tools/stringTools.h"
|
||||
#include "i18n.h"
|
||||
#include "i18n_utils.h"
|
||||
|
||||
namespace kiwix {
|
||||
|
||||
@@ -51,11 +51,12 @@ RequestMethod str2RequestMethod(const std::string& method) {
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
RequestContext::RequestContext(struct MHD_Connection* connection,
|
||||
const std::string& _rootLocation, // URI-encoded
|
||||
RequestContext::RequestContext(const std::string& _rootLocation, // URI-encoded
|
||||
const std::string& unrootedUrl, // URI-decoded
|
||||
const std::string& _method,
|
||||
const std::string& version) :
|
||||
const std::string& version,
|
||||
const NameValuePairs& headers,
|
||||
const NameValuePairs& queryArgs) :
|
||||
rootLocation(_rootLocation),
|
||||
url(unrootedUrl),
|
||||
method(str2RequestMethod(_method)),
|
||||
@@ -64,9 +65,13 @@ RequestContext::RequestContext(struct MHD_Connection* connection,
|
||||
acceptEncodingGzip(false),
|
||||
byteRange_()
|
||||
{
|
||||
MHD_get_connection_values(connection, MHD_HEADER_KIND, &RequestContext::fill_header, this);
|
||||
MHD_get_connection_values(connection, MHD_GET_ARGUMENT_KIND, &RequestContext::fill_argument, this);
|
||||
MHD_get_connection_values(connection, MHD_COOKIE_KIND, &RequestContext::fill_cookie, this);
|
||||
for ( const auto& kv : headers ) {
|
||||
add_header(kv.first, kv.second);
|
||||
}
|
||||
|
||||
for ( const auto& kv : queryArgs ) {
|
||||
add_argument(kv.first, kv.second);
|
||||
}
|
||||
|
||||
try {
|
||||
acceptEncodingGzip =
|
||||
@@ -83,18 +88,14 @@ RequestContext::RequestContext(struct MHD_Connection* connection,
|
||||
RequestContext::~RequestContext()
|
||||
{}
|
||||
|
||||
MHD_Result RequestContext::fill_header(void *__this, enum MHD_ValueKind kind,
|
||||
const char *key, const char *value)
|
||||
void RequestContext::add_header(const char *key, const char *value)
|
||||
{
|
||||
RequestContext *_this = static_cast<RequestContext*>(__this);
|
||||
_this->headers[lcAll(key)] = value;
|
||||
return MHD_YES;
|
||||
this->headers[lcAll(key)] = value;
|
||||
}
|
||||
|
||||
MHD_Result RequestContext::fill_argument(void *__this, enum MHD_ValueKind kind,
|
||||
const char *key, const char* value)
|
||||
void RequestContext::add_argument(const char *key, const char* value)
|
||||
{
|
||||
RequestContext *_this = static_cast<RequestContext*>(__this);
|
||||
RequestContext *_this = this;
|
||||
_this->arguments[key].push_back(value == nullptr ? "" : value);
|
||||
if ( ! _this->queryString.empty() ) {
|
||||
_this->queryString += "&";
|
||||
@@ -104,15 +105,6 @@ MHD_Result RequestContext::fill_argument(void *__this, enum MHD_ValueKind kind,
|
||||
_this->queryString += "=";
|
||||
_this->queryString += urlEncode(value);
|
||||
}
|
||||
return MHD_YES;
|
||||
}
|
||||
|
||||
MHD_Result RequestContext::fill_cookie(void *__this, enum MHD_ValueKind kind,
|
||||
const char *key, const char* value)
|
||||
{
|
||||
RequestContext *_this = static_cast<RequestContext*>(__this);
|
||||
_this->cookies[key] = value == nullptr ? "" : value;
|
||||
return MHD_YES;
|
||||
}
|
||||
|
||||
void RequestContext::print_debug_info() const {
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
#include <stdexcept>
|
||||
|
||||
#include "byte_range.h"
|
||||
#include "tools/stringTools.h"
|
||||
#include "../tools/stringTools.h"
|
||||
|
||||
extern "C" {
|
||||
#include "microhttpd_wrapper.h"
|
||||
@@ -55,12 +55,17 @@ class IndexError: public std::runtime_error {};
|
||||
|
||||
|
||||
class RequestContext {
|
||||
public: // types
|
||||
typedef std::vector<std::pair<const char*, const char*>> NameValuePairs;
|
||||
|
||||
public: // functions
|
||||
RequestContext(struct MHD_Connection* connection,
|
||||
const std::string& rootLocation, // URI-encoded
|
||||
RequestContext(const std::string& rootLocation, // URI-encoded
|
||||
const std::string& unrootedUrl, // URI-decoded
|
||||
const std::string& method,
|
||||
const std::string& version);
|
||||
const std::string& version,
|
||||
const NameValuePairs& headers,
|
||||
const NameValuePairs& queryArgs);
|
||||
|
||||
~RequestContext();
|
||||
|
||||
void print_debug_info() const;
|
||||
@@ -145,16 +150,14 @@ class RequestContext {
|
||||
ByteRange byteRange_;
|
||||
std::map<std::string, std::string> headers;
|
||||
std::map<std::string, std::vector<std::string>> arguments;
|
||||
std::map<std::string, std::string> cookies;
|
||||
std::string queryString;
|
||||
UserLanguage userlang;
|
||||
|
||||
private: // functions
|
||||
UserLanguage determine_user_language() const;
|
||||
|
||||
static MHD_Result fill_header(void *, enum MHD_ValueKind, const char*, const char*);
|
||||
static MHD_Result fill_cookie(void *, enum MHD_ValueKind, const char*, const char*);
|
||||
static MHD_Result fill_argument(void *, enum MHD_ValueKind, const char*, const char*);
|
||||
void add_header(const char* name, const char* value);
|
||||
void add_argument(const char* name, const char* value);
|
||||
};
|
||||
|
||||
template<> std::string RequestContext::get_argument(const std::string& name) const;
|
||||
|
||||
@@ -32,6 +32,9 @@
|
||||
#include <zlib.h>
|
||||
|
||||
#include <array>
|
||||
#include <list>
|
||||
#include <map>
|
||||
#include <regex>
|
||||
|
||||
// This is somehow a magic value.
|
||||
// If this value is too small, we will compress (and lost cpu time) too much
|
||||
@@ -47,6 +50,8 @@ namespace kiwix {
|
||||
|
||||
namespace
|
||||
{
|
||||
typedef kainjow::mustache::data MustacheData;
|
||||
|
||||
// some utilities
|
||||
|
||||
std::string get_mime_type(const zim::Item& item)
|
||||
@@ -119,9 +124,8 @@ const char* getCacheControlHeader(Response::Kind k)
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
Response::Response(bool verbose)
|
||||
: m_verbose(verbose),
|
||||
m_returnCode(MHD_HTTP_OK)
|
||||
Response::Response()
|
||||
: m_returnCode(MHD_HTTP_OK)
|
||||
{
|
||||
add_header(MHD_HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, "*");
|
||||
}
|
||||
@@ -133,14 +137,14 @@ void Response::set_kind(Kind k)
|
||||
m_etag.set_option(ETag::ZIM_CONTENT);
|
||||
}
|
||||
|
||||
std::unique_ptr<Response> Response::build(const InternalServer& server)
|
||||
std::unique_ptr<Response> Response::build()
|
||||
{
|
||||
return std::unique_ptr<Response>(new Response(server.m_verbose.load()));
|
||||
return std::make_unique<Response>();
|
||||
}
|
||||
|
||||
std::unique_ptr<Response> Response::build_304(const InternalServer& server, const ETag& etag)
|
||||
std::unique_ptr<Response> Response::build_304(const ETag& etag)
|
||||
{
|
||||
auto response = Response::build(server);
|
||||
auto response = Response::build();
|
||||
response->set_code(MHD_HTTP_NOT_MODIFIED);
|
||||
response->m_etag = etag;
|
||||
if ( etag.get_option(ETag::ZIM_CONTENT) ) {
|
||||
@@ -152,67 +156,316 @@ std::unique_ptr<Response> Response::build_304(const InternalServer& server, cons
|
||||
return response;
|
||||
}
|
||||
|
||||
const UrlNotFoundMsg urlNotFoundMsg;
|
||||
const InvalidUrlMsg invalidUrlMsg;
|
||||
|
||||
std::string ContentResponseBlueprint::getMessage(const std::string& msgId) const
|
||||
namespace
|
||||
{
|
||||
return getTranslatedString(m_request.get_user_language(), msgId);
|
||||
|
||||
// This class was introduced in order to work around the missing support
|
||||
// for std::variant (and std::optional) under some of the current build
|
||||
// platforms.
|
||||
template<class T>
|
||||
class Optional
|
||||
{
|
||||
public: // functions
|
||||
Optional() {}
|
||||
Optional(const T& t) : ptr(new T(t)) {}
|
||||
Optional(const Optional& o) : ptr(o.has_value() ? new T(*o) : nullptr) {}
|
||||
Optional(Optional&& o) : ptr(std::move(o.ptr)) {}
|
||||
|
||||
Optional& operator=(const Optional& o)
|
||||
{
|
||||
*this = Optional(o);
|
||||
return *this;
|
||||
}
|
||||
|
||||
Optional& operator=(Optional&& o)
|
||||
{
|
||||
ptr = std::move(o.ptr);
|
||||
return *this;
|
||||
}
|
||||
|
||||
bool has_value() const { return ptr.get() != nullptr; }
|
||||
const T& operator*() const { return *ptr; }
|
||||
T& operator*() { return *ptr; }
|
||||
|
||||
private: // data
|
||||
std::unique_ptr<T> ptr;
|
||||
};
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
class ContentResponseBlueprint::Data
|
||||
{
|
||||
public:
|
||||
typedef std::list<Data> List;
|
||||
typedef std::map<std::string, Data> Object;
|
||||
|
||||
private:
|
||||
// std::variant<std::string, bool, List, Object> data;
|
||||
// XXX: libkiwix is compiled on platforms where std::variant
|
||||
// XXX: is not yet supported. Hence this hack. Only one
|
||||
// XXX: of the below data members is expected to contain a value.
|
||||
Optional<std::string> m_stringValue;
|
||||
Optional<bool> m_boolValue;
|
||||
Optional<List> m_listValue;
|
||||
Optional<Object> m_objectValue;
|
||||
|
||||
public:
|
||||
Data() {}
|
||||
Data(const std::string& s) : m_stringValue(s) {}
|
||||
Data(bool b) : m_boolValue(b) {}
|
||||
Data(const List& l) : m_listValue(l) {}
|
||||
Data(const Object& o) : m_objectValue(o) {}
|
||||
|
||||
MustacheData toMustache(const std::string& lang) const;
|
||||
|
||||
Data& operator[](const std::string& key)
|
||||
{
|
||||
return (*m_objectValue)[key];
|
||||
}
|
||||
|
||||
void push_back(const Data& d) { (*m_listValue).push_back(d); }
|
||||
|
||||
static Data onlyAsNonEmptyValue(const std::string& s)
|
||||
{
|
||||
return s.empty() ? Data(false) : Data(s);
|
||||
}
|
||||
|
||||
static Data from(const ParameterizedMessage& pmsg)
|
||||
{
|
||||
Object obj;
|
||||
for(const auto& kv : pmsg.getParams()) {
|
||||
obj[kv.first] = kv.second;
|
||||
}
|
||||
return Object{
|
||||
{ "msgid", pmsg.getMsgId() },
|
||||
{ "params", Data(obj) }
|
||||
};
|
||||
}
|
||||
|
||||
static Data fromMsgId(const std::string& nonParameterizedMsgId)
|
||||
{
|
||||
return from(nonParameterizedMessage(nonParameterizedMsgId));
|
||||
}
|
||||
|
||||
static Data staticMultiParagraphText(const std::string& msgIdPrefix, size_t n)
|
||||
{
|
||||
Object paragraphs;
|
||||
for ( size_t i = 1; i <= n; ++i ) {
|
||||
std::ostringstream oss;
|
||||
oss << "p" << i;
|
||||
const std::string pId = oss.str();
|
||||
paragraphs[pId] = fromMsgId(msgIdPrefix + "." + pId);
|
||||
}
|
||||
return paragraphs;
|
||||
}
|
||||
|
||||
std::string asJSON() const;
|
||||
void dumpJSON(std::ostream& os) const;
|
||||
|
||||
private:
|
||||
bool isString() const { return m_stringValue.has_value(); }
|
||||
bool isList() const { return m_listValue.has_value(); }
|
||||
bool isObject() const { return m_objectValue.has_value(); }
|
||||
|
||||
const std::string& stringValue() const { return *m_stringValue; }
|
||||
bool boolValue() const { return *m_boolValue; }
|
||||
const List& listValue() const { return *m_listValue; }
|
||||
const Object& objectValue() const { return *m_objectValue; }
|
||||
|
||||
const Data* get(const std::string& key) const
|
||||
{
|
||||
if ( !isObject() )
|
||||
return nullptr;
|
||||
|
||||
const auto& obj = objectValue();
|
||||
const auto it = obj.find(key);
|
||||
return it != obj.end() ? &it->second : nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
MustacheData ContentResponseBlueprint::Data::toMustache(const std::string& lang) const
|
||||
{
|
||||
if ( this->isList() ) {
|
||||
kainjow::mustache::list l;
|
||||
for ( const auto& x : this->listValue() ) {
|
||||
l.push_back(x.toMustache(lang));
|
||||
}
|
||||
return l;
|
||||
} else if ( this->isObject() ) {
|
||||
const Data* msgId = this->get("msgid");
|
||||
const Data* msgParams = this->get("params");
|
||||
if ( msgId && msgId->isString() && msgParams && msgParams->isObject() ) {
|
||||
std::map<std::string, std::string> params;
|
||||
for(const auto& kv : msgParams->objectValue()) {
|
||||
params[kv.first] = kv.second.stringValue();
|
||||
}
|
||||
const ParameterizedMessage msg(msgId->stringValue(), ParameterizedMessage::Parameters(params));
|
||||
return msg.getText(lang);
|
||||
} else {
|
||||
kainjow::mustache::object o;
|
||||
for ( const auto& kv : this->objectValue() ) {
|
||||
o[kv.first] = kv.second.toMustache(lang);
|
||||
}
|
||||
return o;
|
||||
}
|
||||
} else if ( this->isString() ) {
|
||||
return this->stringValue();
|
||||
} else {
|
||||
return this->boolValue();
|
||||
}
|
||||
}
|
||||
|
||||
void ContentResponseBlueprint::Data::dumpJSON(std::ostream& os) const
|
||||
{
|
||||
if ( this->isString() ) {
|
||||
os << '"' << escapeForJSON(this->stringValue()) << '"';
|
||||
} else if ( this->isList() ) {
|
||||
const char * sep = " ";
|
||||
os << "[";
|
||||
|
||||
for ( const auto& x : this->listValue() ) {
|
||||
os << sep;
|
||||
x.dumpJSON(os);
|
||||
sep = ", ";
|
||||
}
|
||||
os << " ]";
|
||||
} else if ( this->isObject() ) {
|
||||
const char * sep = " ";
|
||||
os << "{";
|
||||
for ( const auto& kv : this->objectValue() ) {
|
||||
os << sep << '"' << kv.first << "\" : ";
|
||||
kv.second.dumpJSON(os);
|
||||
sep = ", ";
|
||||
}
|
||||
os << " }";
|
||||
} else {
|
||||
os << (this->boolValue() ? "true" : "false");
|
||||
}
|
||||
}
|
||||
|
||||
std::string ContentResponseBlueprint::Data::asJSON() const
|
||||
{
|
||||
std::ostringstream oss;
|
||||
this->dumpJSON(oss);
|
||||
|
||||
// This JSON is going to be used in HTML inside a <script></script> tag.
|
||||
// If it contains "</script>" (or "</script >") as a substring, then the HTML
|
||||
// parser will be confused. Since for a valid JSON that may happen only inside
|
||||
// a JSON string, we can safely take advantage of the answers to
|
||||
// https://stackoverflow.com/questions/28259389/how-to-put-script-in-a-javascript-string
|
||||
// and work around the issue by inserting an otherwise harmless backslash.
|
||||
return std::regex_replace(oss.str(), std::regex("</script"), "</scr\\ipt");
|
||||
}
|
||||
|
||||
ContentResponseBlueprint::ContentResponseBlueprint(const RequestContext* request,
|
||||
int httpStatusCode,
|
||||
const std::string& mimeType,
|
||||
const std::string& templateStr,
|
||||
bool includeKiwixResponseData)
|
||||
: m_request(*request)
|
||||
, m_httpStatusCode(httpStatusCode)
|
||||
, m_mimeType(mimeType)
|
||||
, m_template(templateStr)
|
||||
, m_includeKiwixResponseData(includeKiwixResponseData)
|
||||
, m_data(new Data)
|
||||
{}
|
||||
|
||||
ContentResponseBlueprint::~ContentResponseBlueprint() = default;
|
||||
|
||||
std::unique_ptr<ContentResponse> ContentResponseBlueprint::generateResponseObject() const
|
||||
{
|
||||
auto r = ContentResponse::build(m_server, m_template, m_data, m_mimeType);
|
||||
kainjow::mustache::data d = m_data->toMustache(m_request.get_user_language());
|
||||
if ( m_includeKiwixResponseData ) {
|
||||
d.set("KIWIX_RESPONSE_TEMPLATE", escapeForJSON(m_template, false));
|
||||
d.set("KIWIX_RESPONSE_DATA", m_data->asJSON());
|
||||
}
|
||||
auto r = ContentResponse::build(m_template, d, m_mimeType);
|
||||
r->set_code(m_httpStatusCode);
|
||||
return r;
|
||||
}
|
||||
|
||||
HTTPErrorResponse::HTTPErrorResponse(const InternalServer& server,
|
||||
const RequestContext& request,
|
||||
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,
|
||||
const std::string& headingMsgId,
|
||||
const std::string& cssUrl)
|
||||
: ContentResponseBlueprint(&server,
|
||||
&request,
|
||||
const std::string& cssUrl,
|
||||
bool includeKiwixResponseData)
|
||||
: ContentResponseBlueprint(&request,
|
||||
httpStatusCode,
|
||||
request.get_requested_format() == "html" ? "text/html; charset=utf-8" : "application/xml; charset=utf-8",
|
||||
request.get_requested_format() == "html" ? RESOURCE::templates::error_html : RESOURCE::templates::error_xml)
|
||||
request.get_requested_format() == "html" ? RESOURCE::templates::error_html : RESOURCE::templates::error_xml,
|
||||
includeKiwixResponseData)
|
||||
{
|
||||
kainjow::mustache::list emptyList;
|
||||
this->m_data = kainjow::mustache::object{
|
||||
{"CSS_URL", onlyAsNonEmptyMustacheValue(cssUrl) },
|
||||
{"PAGE_TITLE", getMessage(pageTitleMsgId)},
|
||||
{"PAGE_HEADING", getMessage(headingMsgId)},
|
||||
Data::List emptyList;
|
||||
*this->m_data = Data(Data::Object{
|
||||
{"CSS_URL", Data::onlyAsNonEmptyValue(cssUrl) },
|
||||
{"PAGE_TITLE", Data::fromMsgId(pageTitleMsgId)},
|
||||
{"PAGE_HEADING", Data::fromMsgId(headingMsgId)},
|
||||
{"details", emptyList}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
HTTP404Response::HTTP404Response(const InternalServer& server,
|
||||
const RequestContext& request)
|
||||
: HTTPErrorResponse(server,
|
||||
request,
|
||||
HTTP404Response::HTTP404Response(const RequestContext& request)
|
||||
: HTTPErrorResponse(request,
|
||||
MHD_HTTP_NOT_FOUND,
|
||||
"404-page-title",
|
||||
"404-page-heading")
|
||||
"404-page-heading",
|
||||
std::string(),
|
||||
/*includeKiwixResponseData=*/true)
|
||||
{
|
||||
}
|
||||
|
||||
HTTPErrorResponse& HTTP404Response::operator+(UrlNotFoundMsg /*unused*/)
|
||||
UrlNotFoundResponse::UrlNotFoundResponse(const RequestContext& request)
|
||||
: HTTP404Response(request)
|
||||
{
|
||||
const std::string requestUrl = urlDecode(m_request.get_full_url(), false);
|
||||
return *this + ParameterizedMessage("url-not-found", {{"url", requestUrl}});
|
||||
}
|
||||
|
||||
HTTPErrorResponse& HTTPErrorResponse::operator+(const std::string& msg)
|
||||
{
|
||||
m_data["details"].push_back({"p", msg});
|
||||
return *this;
|
||||
*this += ParameterizedMessage("url-not-found", {{"url", requestUrl}});
|
||||
}
|
||||
|
||||
HTTPErrorResponse& HTTPErrorResponse::operator+(const ParameterizedMessage& details)
|
||||
{
|
||||
return *this + details.getText(m_request.get_user_language());
|
||||
(*m_data)["details"].push_back(Data::Object{{"p", Data::from(details)}});
|
||||
return *this;
|
||||
}
|
||||
|
||||
HTTPErrorResponse& HTTPErrorResponse::operator+=(const ParameterizedMessage& details)
|
||||
@@ -222,50 +475,51 @@ HTTPErrorResponse& HTTPErrorResponse::operator+=(const ParameterizedMessage& det
|
||||
}
|
||||
|
||||
|
||||
HTTP400Response::HTTP400Response(const InternalServer& server,
|
||||
const RequestContext& request)
|
||||
: HTTPErrorResponse(server,
|
||||
request,
|
||||
HTTP400Response::HTTP400Response(const RequestContext& request)
|
||||
: HTTPErrorResponse(request,
|
||||
MHD_HTTP_BAD_REQUEST,
|
||||
"400-page-title",
|
||||
"400-page-heading")
|
||||
{
|
||||
}
|
||||
|
||||
HTTPErrorResponse& HTTP400Response::operator+(InvalidUrlMsg /*unused*/)
|
||||
"400-page-heading",
|
||||
std::string(),
|
||||
/*includeKiwixResponseData=*/true)
|
||||
{
|
||||
std::string requestUrl = urlDecode(m_request.get_full_url(), false);
|
||||
const auto query = m_request.get_query();
|
||||
if (!query.empty()) {
|
||||
requestUrl += "?" + encodeDiples(query);
|
||||
}
|
||||
kainjow::mustache::mustache msgTmpl(R"(The requested URL "{{{url}}}" is not a valid request.)");
|
||||
return *this + msgTmpl.render({"url", requestUrl});
|
||||
*this += ParameterizedMessage("invalid-request", {{"url", requestUrl}});
|
||||
}
|
||||
|
||||
HTTP500Response::HTTP500Response(const InternalServer& server,
|
||||
const RequestContext& request)
|
||||
: HTTPErrorResponse(server,
|
||||
request,
|
||||
MHD_HTTP_INTERNAL_SERVER_ERROR,
|
||||
"500-page-title",
|
||||
"500-page-heading")
|
||||
HTTP500Response::HTTP500Response(const RequestContext& request,
|
||||
const std::string& root,
|
||||
const std::string& urlPath,
|
||||
const std::string& errorText)
|
||||
: ContentResponseBlueprint(&request,
|
||||
MHD_HTTP_INTERNAL_SERVER_ERROR,
|
||||
"text/html; charset=utf-8",
|
||||
RESOURCE::templates::sexy500_html,
|
||||
/*includeKiwixResponseData=*/true)
|
||||
{
|
||||
// operator+() is a state-modifying operator (akin to operator+=)
|
||||
*this + "An internal server error occured. We are sorry about that :/";
|
||||
auto pageParams = Data::Object{
|
||||
{"root", root },
|
||||
{"url_path", urlPath},
|
||||
{"PAGE_TITLE", Data::fromMsgId("500-page-title")},
|
||||
{"PAGE_HEADING", Data::fromMsgId("500-page-heading")},
|
||||
{"PAGE_TEXT", Data::fromMsgId("500-page-text")},
|
||||
{"500_img_text", Data::fromMsgId("500-img-text")},
|
||||
};
|
||||
|
||||
if ( !errorText.empty() ) {
|
||||
pageParams["error"] = errorText;
|
||||
}
|
||||
|
||||
*this->m_data = Data(pageParams);
|
||||
}
|
||||
|
||||
std::unique_ptr<ContentResponse> HTTP500Response::generateResponseObject() const
|
||||
std::unique_ptr<Response> Response::build_416(size_t resourceLength)
|
||||
{
|
||||
const std::string mimeType = "text/html;charset=utf-8";
|
||||
auto r = ContentResponse::build(m_server, m_template, m_data, mimeType);
|
||||
r->set_code(m_httpStatusCode);
|
||||
return r;
|
||||
}
|
||||
|
||||
std::unique_ptr<Response> Response::build_416(const InternalServer& server, size_t resourceLength)
|
||||
{
|
||||
auto response = Response::build(server);
|
||||
auto response = Response::build();
|
||||
// [FIXME] (compile with recent enough version of libmicrohttpd)
|
||||
// response->set_code(MHD_HTTP_RANGE_NOT_SATISFIABLE);
|
||||
response->set_code(416);
|
||||
@@ -277,9 +531,9 @@ std::unique_ptr<Response> Response::build_416(const InternalServer& server, size
|
||||
}
|
||||
|
||||
|
||||
std::unique_ptr<Response> Response::build_redirect(const InternalServer& server, const std::string& redirectUrl)
|
||||
std::unique_ptr<Response> Response::build_redirect(const std::string& redirectUrl)
|
||||
{
|
||||
auto response = Response::build(server);
|
||||
auto response = Response::build();
|
||||
response->m_returnCode = MHD_HTTP_FOUND;
|
||||
response->add_header(MHD_HTTP_HEADER_LOCATION, redirectUrl);
|
||||
return response;
|
||||
@@ -374,7 +628,7 @@ ContentResponse::create_mhd_response(const RequestContext& request)
|
||||
return response;
|
||||
}
|
||||
|
||||
MHD_Result Response::send(const RequestContext& request, MHD_Connection* connection)
|
||||
MHD_Result Response::send(const RequestContext& request, bool verbose, MHD_Connection* connection)
|
||||
{
|
||||
MHD_Response* response = create_mhd_response(request);
|
||||
|
||||
@@ -390,7 +644,7 @@ MHD_Result Response::send(const RequestContext& request, MHD_Connection* connect
|
||||
if (m_returnCode == MHD_HTTP_OK && m_byteRange.kind() == ByteRange::RESOLVED_PARTIAL_CONTENT)
|
||||
m_returnCode = MHD_HTTP_PARTIAL_CONTENT;
|
||||
|
||||
if (m_verbose)
|
||||
if (verbose)
|
||||
print_response_info(m_returnCode, response);
|
||||
|
||||
auto ret = MHD_queue_response(connection, m_returnCode, response);
|
||||
@@ -398,9 +652,8 @@ MHD_Result Response::send(const RequestContext& request, MHD_Connection* connect
|
||||
return ret;
|
||||
}
|
||||
|
||||
ContentResponse::ContentResponse(const std::string& root, bool verbose, const std::string& content, const std::string& mimetype) :
|
||||
Response(verbose),
|
||||
m_root(root),
|
||||
ContentResponse::ContentResponse(const std::string& content, const std::string& mimetype) :
|
||||
Response(),
|
||||
m_content(content),
|
||||
m_mimeType(mimetype)
|
||||
{
|
||||
@@ -408,29 +661,23 @@ ContentResponse::ContentResponse(const std::string& root, bool verbose, const st
|
||||
}
|
||||
|
||||
std::unique_ptr<ContentResponse> ContentResponse::build(
|
||||
const InternalServer& server,
|
||||
const std::string& content,
|
||||
const std::string& mimetype)
|
||||
{
|
||||
return std::unique_ptr<ContentResponse>(new ContentResponse(
|
||||
server.m_root,
|
||||
server.m_verbose.load(),
|
||||
content,
|
||||
mimetype));
|
||||
return std::make_unique<ContentResponse>(content, mimetype);
|
||||
}
|
||||
|
||||
std::unique_ptr<ContentResponse> ContentResponse::build(
|
||||
const InternalServer& server,
|
||||
const std::string& template_str,
|
||||
kainjow::mustache::data data,
|
||||
const std::string& mimetype)
|
||||
{
|
||||
auto content = render_template(template_str, data);
|
||||
return ContentResponse::build(server, content, mimetype);
|
||||
return ContentResponse::build(content, mimetype);
|
||||
}
|
||||
|
||||
ItemResponse::ItemResponse(bool verbose, const zim::Item& item, const std::string& mimetype, const ByteRange& byterange) :
|
||||
Response(verbose),
|
||||
ItemResponse::ItemResponse(const zim::Item& item, const std::string& mimetype, const ByteRange& byterange) :
|
||||
Response(),
|
||||
m_item(item),
|
||||
m_mimeType(mimetype)
|
||||
{
|
||||
@@ -439,30 +686,26 @@ ItemResponse::ItemResponse(bool verbose, const zim::Item& item, const std::strin
|
||||
add_header(MHD_HTTP_HEADER_CONTENT_TYPE, m_mimeType);
|
||||
}
|
||||
|
||||
std::unique_ptr<Response> ItemResponse::build(const InternalServer& server, const RequestContext& request, const zim::Item& item)
|
||||
std::unique_ptr<Response> ItemResponse::build(const RequestContext& request, const zim::Item& item)
|
||||
{
|
||||
const std::string mimetype = get_mime_type(item);
|
||||
auto byteRange = request.get_range().resolve(item.getSize());
|
||||
const bool noRange = byteRange.kind() == ByteRange::RESOLVED_FULL_CONTENT;
|
||||
if (noRange && is_compressible_mime_type(mimetype)) {
|
||||
// Return a contentResponse
|
||||
auto response = ContentResponse::build(server, item.getData(), mimetype);
|
||||
auto response = ContentResponse::build(item.getData(), mimetype);
|
||||
response->set_kind(Response::ZIM_CONTENT);
|
||||
response->m_byteRange = byteRange;
|
||||
return std::move(response);
|
||||
}
|
||||
|
||||
if (byteRange.kind() == ByteRange::RESOLVED_UNSATISFIABLE) {
|
||||
auto response = Response::build_416(server, item.getSize());
|
||||
auto response = Response::build_416(item.getSize());
|
||||
response->set_kind(Response::ZIM_CONTENT);
|
||||
return response;
|
||||
}
|
||||
|
||||
return std::unique_ptr<Response>(new ItemResponse(
|
||||
server.m_verbose.load(),
|
||||
item,
|
||||
mimetype,
|
||||
byteRange));
|
||||
return std::make_unique<ItemResponse>(item, mimetype, byteRange);
|
||||
}
|
||||
|
||||
MHD_Response*
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -41,7 +41,6 @@ class Archive;
|
||||
|
||||
namespace kiwix {
|
||||
|
||||
class InternalServer;
|
||||
class RequestContext;
|
||||
|
||||
class Response {
|
||||
@@ -54,15 +53,15 @@ class Response {
|
||||
};
|
||||
|
||||
public:
|
||||
Response(bool verbose);
|
||||
Response();
|
||||
virtual ~Response() = default;
|
||||
|
||||
static std::unique_ptr<Response> build(const InternalServer& server);
|
||||
static std::unique_ptr<Response> build_304(const InternalServer& server, const ETag& etag);
|
||||
static std::unique_ptr<Response> build_416(const InternalServer& server, size_t resourceLength);
|
||||
static std::unique_ptr<Response> build_redirect(const InternalServer& server, const std::string& redirectUrl);
|
||||
static std::unique_ptr<Response> build();
|
||||
static std::unique_ptr<Response> build_304(const ETag& etag);
|
||||
static std::unique_ptr<Response> build_416(size_t resourceLength);
|
||||
static std::unique_ptr<Response> build_redirect(const std::string& redirectUrl);
|
||||
|
||||
MHD_Result send(const RequestContext& request, MHD_Connection* connection);
|
||||
MHD_Result send(const RequestContext& request, bool verbose, MHD_Connection* connection);
|
||||
|
||||
void set_code(int code) { m_returnCode = code; }
|
||||
void set_kind(Kind k);
|
||||
@@ -78,7 +77,6 @@ class Response {
|
||||
|
||||
protected: // data
|
||||
Kind m_kind = DYNAMIC_CONTENT;
|
||||
bool m_verbose;
|
||||
int m_returnCode;
|
||||
ByteRange m_byteRange;
|
||||
ETag m_etag;
|
||||
@@ -91,22 +89,21 @@ class Response {
|
||||
class ContentResponse : public Response {
|
||||
public:
|
||||
ContentResponse(
|
||||
const std::string& root,
|
||||
bool verbose,
|
||||
const std::string& content,
|
||||
const std::string& mimetype);
|
||||
|
||||
static std::unique_ptr<ContentResponse> build(
|
||||
const InternalServer& server,
|
||||
const std::string& content,
|
||||
const std::string& mimetype);
|
||||
|
||||
static std::unique_ptr<ContentResponse> build(
|
||||
const InternalServer& server,
|
||||
const std::string& template_str,
|
||||
kainjow::mustache::data data,
|
||||
const std::string& mimetype);
|
||||
|
||||
const std::string& getContent() const { return m_content; }
|
||||
const std::string& getMimeType() const { return m_mimeType; }
|
||||
|
||||
private:
|
||||
MHD_Response* create_mhd_response(const RequestContext& request);
|
||||
|
||||
@@ -114,7 +111,6 @@ class ContentResponse : public Response {
|
||||
|
||||
|
||||
private:
|
||||
std::string m_root;
|
||||
std::string m_content;
|
||||
std::string m_mimeType;
|
||||
};
|
||||
@@ -122,99 +118,80 @@ class ContentResponse : public Response {
|
||||
class ContentResponseBlueprint
|
||||
{
|
||||
public: // functions
|
||||
ContentResponseBlueprint(const InternalServer* server,
|
||||
const RequestContext* request,
|
||||
ContentResponseBlueprint(const RequestContext* request,
|
||||
int httpStatusCode,
|
||||
const std::string& mimeType,
|
||||
const std::string& templateStr)
|
||||
: m_server(*server)
|
||||
, m_request(*request)
|
||||
, m_httpStatusCode(httpStatusCode)
|
||||
, m_mimeType(mimeType)
|
||||
, m_template(templateStr)
|
||||
{}
|
||||
const std::string& templateStr,
|
||||
bool includeKiwixResponseData = false);
|
||||
|
||||
virtual ~ContentResponseBlueprint() = default;
|
||||
~ContentResponseBlueprint();
|
||||
|
||||
operator std::unique_ptr<ContentResponse>() const
|
||||
operator std::unique_ptr<Response>() const
|
||||
{
|
||||
return generateResponseObject();
|
||||
}
|
||||
|
||||
operator std::unique_ptr<Response>() const
|
||||
{
|
||||
return operator std::unique_ptr<ContentResponse>();
|
||||
}
|
||||
std::unique_ptr<ContentResponse> generateResponseObject() const;
|
||||
|
||||
protected: // types
|
||||
class Data;
|
||||
|
||||
protected: // functions
|
||||
std::string getMessage(const std::string& msgId) const;
|
||||
virtual std::unique_ptr<ContentResponse> generateResponseObject() const;
|
||||
|
||||
public: //data
|
||||
const InternalServer& m_server;
|
||||
protected: //data
|
||||
const RequestContext& m_request;
|
||||
const int m_httpStatusCode;
|
||||
const std::string m_mimeType;
|
||||
const std::string m_template;
|
||||
kainjow::mustache::data m_data;
|
||||
const bool m_includeKiwixResponseData;
|
||||
std::unique_ptr<Data> m_data;
|
||||
};
|
||||
|
||||
struct NewHTTP404Response : ContentResponseBlueprint
|
||||
{
|
||||
NewHTTP404Response(const RequestContext& request,
|
||||
const std::string& root,
|
||||
const std::string& urlPath);
|
||||
};
|
||||
|
||||
struct HTTPErrorResponse : ContentResponseBlueprint
|
||||
{
|
||||
HTTPErrorResponse(const InternalServer& server,
|
||||
const RequestContext& request,
|
||||
HTTPErrorResponse(const RequestContext& request,
|
||||
int httpStatusCode,
|
||||
const std::string& pageTitleMsgId,
|
||||
const std::string& headingMsgId,
|
||||
const std::string& cssUrl = "");
|
||||
const std::string& cssUrl = "",
|
||||
bool includeKiwixResponseData = false);
|
||||
|
||||
HTTPErrorResponse& operator+(const std::string& msg);
|
||||
HTTPErrorResponse& operator+(const ParameterizedMessage& errorDetails);
|
||||
HTTPErrorResponse& operator+=(const ParameterizedMessage& errorDetails);
|
||||
};
|
||||
|
||||
class UrlNotFoundMsg {};
|
||||
|
||||
extern const UrlNotFoundMsg urlNotFoundMsg;
|
||||
|
||||
struct HTTP404Response : HTTPErrorResponse
|
||||
{
|
||||
HTTP404Response(const InternalServer& server,
|
||||
const RequestContext& request);
|
||||
|
||||
using HTTPErrorResponse::operator+;
|
||||
HTTPErrorResponse& operator+(UrlNotFoundMsg /*unused*/);
|
||||
explicit HTTP404Response(const RequestContext& request);
|
||||
};
|
||||
|
||||
class InvalidUrlMsg {};
|
||||
|
||||
extern const InvalidUrlMsg invalidUrlMsg;
|
||||
struct UrlNotFoundResponse : HTTP404Response
|
||||
{
|
||||
explicit UrlNotFoundResponse(const RequestContext& request);
|
||||
};
|
||||
|
||||
struct HTTP400Response : HTTPErrorResponse
|
||||
{
|
||||
HTTP400Response(const InternalServer& server,
|
||||
const RequestContext& request);
|
||||
|
||||
using HTTPErrorResponse::operator+;
|
||||
HTTPErrorResponse& operator+(InvalidUrlMsg /*unused*/);
|
||||
explicit HTTP400Response(const RequestContext& request);
|
||||
};
|
||||
|
||||
struct HTTP500Response : HTTPErrorResponse
|
||||
struct HTTP500Response : ContentResponseBlueprint
|
||||
{
|
||||
HTTP500Response(const InternalServer& server,
|
||||
const RequestContext& request);
|
||||
|
||||
private: // overrides
|
||||
// generateResponseObject() is overriden in order to produce a minimal
|
||||
// response without any need for additional resources from the server
|
||||
std::unique_ptr<ContentResponse> generateResponseObject() const override;
|
||||
HTTP500Response(const RequestContext& request,
|
||||
const std::string& root,
|
||||
const std::string& urlPath,
|
||||
const std::string& error = "");
|
||||
};
|
||||
|
||||
class ItemResponse : public Response {
|
||||
public:
|
||||
ItemResponse(bool verbose, const zim::Item& item, const std::string& mimetype, const ByteRange& byterange);
|
||||
static std::unique_ptr<Response> build(const InternalServer& server, const RequestContext& request, const zim::Item& item);
|
||||
ItemResponse(const zim::Item& item, const std::string& mimetype, const ByteRange& byterange);
|
||||
static std::unique_ptr<Response> build(const RequestContext& request, const zim::Item& item);
|
||||
|
||||
private:
|
||||
MHD_Response* create_mhd_response(const RequestContext& request);
|
||||
@@ -223,6 +200,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
|
||||
|
||||
108
src/spelling_correction.cpp
Normal file
108
src/spelling_correction.cpp
Normal file
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Veloman Yunkan
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||
* MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "spelling_correction.h"
|
||||
#include "zim/archive.h"
|
||||
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
|
||||
#include <xapian.h>
|
||||
|
||||
namespace kiwix
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
std::vector<std::string> getAllTitles(const zim::Archive& a)
|
||||
{
|
||||
std::vector<std::string> result;
|
||||
for (const auto& entry : a.iterByPath() ) {
|
||||
result.push_back(entry.getTitle());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void createXapianDB(std::string path, const zim::Archive& archive)
|
||||
{
|
||||
const int flags = Xapian::DB_BACKEND_GLASS|Xapian::DB_CREATE;
|
||||
const auto tmpDbPath = path + ".tmp";
|
||||
Xapian::WritableDatabase db(tmpDbPath, flags);
|
||||
for (const auto& t : getAllTitles(archive)) {
|
||||
db.add_spelling(t);
|
||||
}
|
||||
db.commit();
|
||||
db.compact(path, Xapian::DBCOMPACT_SINGLE_FILE);
|
||||
db.close();
|
||||
std::filesystem::remove_all(tmpDbPath);
|
||||
}
|
||||
|
||||
std::string spellingsDBPathForZIMArchive(std::filesystem::path cacheDirPath, const zim::Archive& a)
|
||||
{
|
||||
// The version of spellings DB must be updated each time an important change
|
||||
// to the implementation is made that renders using the previous version
|
||||
// impossible or undesirable.
|
||||
const char SPELLINGS_DB_VERSION[] = "0.1";
|
||||
|
||||
std::ostringstream filename;
|
||||
filename << a.getUuid() << ".spellingsdb.v" << SPELLINGS_DB_VERSION;
|
||||
return (cacheDirPath / filename.str()).string();
|
||||
}
|
||||
|
||||
std::unique_ptr<Xapian::Database> openOrCreateXapianDB(std::filesystem::path cacheDirPath, const zim::Archive& archive)
|
||||
{
|
||||
const auto path = spellingsDBPathForZIMArchive(cacheDirPath, archive);
|
||||
try
|
||||
{
|
||||
return std::make_unique<Xapian::Database>(path);
|
||||
}
|
||||
catch (const Xapian::DatabaseOpeningError& )
|
||||
{
|
||||
createXapianDB(path, archive);
|
||||
return std::make_unique<Xapian::Database>(path);
|
||||
}
|
||||
}
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
SpellingsDB::SpellingsDB(const zim::Archive& archive, std::filesystem::path cacheDirPath)
|
||||
: impl_(openOrCreateXapianDB(cacheDirPath, archive))
|
||||
{
|
||||
}
|
||||
|
||||
SpellingsDB::~SpellingsDB()
|
||||
{
|
||||
}
|
||||
|
||||
std::vector<std::string> SpellingsDB::getSpellingCorrections(const std::string& word, uint32_t maxCount) const
|
||||
{
|
||||
if ( maxCount > 1 ) {
|
||||
throw std::runtime_error("More than one spelling correction was requested");
|
||||
}
|
||||
|
||||
std::vector<std::string> result;
|
||||
const auto term = impl_->get_spelling_suggestion(word, 3);
|
||||
if ( !term.empty() ) {
|
||||
result.push_back(term);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace kiwix
|
||||
@@ -10,10 +10,12 @@ namespace
|
||||
|
||||
// These mappings are not provided by the ICU library, any such mappings can be manually added here
|
||||
std::map<std::string, std::string> iso639_3 = {
|
||||
{"ami", "Amis"},
|
||||
{"atj", "atikamekw"},
|
||||
{"azb", "آذربایجان دیلی"},
|
||||
{"bcl", "central bikol"},
|
||||
{"bgs", "tagabawa"},
|
||||
{"blk", "ပအိုဝ်ႏ"},
|
||||
{"bxr", "буряад хэлэн"},
|
||||
{"cbk", "chavacano"},
|
||||
{"cdo", "閩東語"},
|
||||
@@ -23,13 +25,16 @@ std::map<std::string, std::string> iso639_3 = {
|
||||
{"eml", "emiliân-rumagnōl"},
|
||||
{"fbs", "српскохрватски"},
|
||||
{"fon", "fɔ̀ngbè"},
|
||||
{"gcr", "Kriyòl gwiyannen"},
|
||||
{"guw", "Gungbe"},
|
||||
{"hbs", "srpskohrvatski"},
|
||||
{"hyw", "հայերէն/հայերեն"},
|
||||
{"ido", "ido"},
|
||||
{"kbp", "kabɩyɛ"},
|
||||
{"kld", "Gamilaraay"},
|
||||
{"lbe", "лакку маз"},
|
||||
{"lbj", "ལ་དྭགས་སྐད་"},
|
||||
{"lld", "ladin"},
|
||||
{"map", "Austronesian"},
|
||||
{"mhr", "марий йылме"},
|
||||
{"mnw", "ဘာသာမန်"},
|
||||
@@ -41,10 +46,15 @@ std::map<std::string, std::string> iso639_3 = {
|
||||
{"olo", "livvi"},
|
||||
{"pih", "Pitcairn-Norfolk"},
|
||||
{"pnb", "Western Panjabi"},
|
||||
{"pwn", "Pinayuanan"},
|
||||
{"rmr", "Caló"},
|
||||
{"rmy", "romani shib"},
|
||||
{"roa", "romance languages"},
|
||||
{"twi", "twi"},
|
||||
{"skr", "سرائیکی"},
|
||||
{"szy", "Sakizaya"},
|
||||
{"tay", "Tayal"},
|
||||
{"tgl", "Wikang Tagalog"},
|
||||
{"twi", "Akwapem Twi"},
|
||||
// ICU for Ubuntu versions <= focal (20.04) returns "" for the language code ""
|
||||
// unlike the later versions - which returns "und". We map this value to "Undetermined" for a common ground.
|
||||
{"", "Undetermined"},
|
||||
@@ -58,6 +68,7 @@ void fillLanguagesMap()
|
||||
const kiwix::ICULanguageInfo lang(*icuLangPtr);
|
||||
iso639_3.insert({lang.iso3Code(), lang.selfName()});
|
||||
}
|
||||
iso639_3.erase("mul");
|
||||
}
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
*/
|
||||
|
||||
#include "tools.h"
|
||||
#include "stringTools.h"
|
||||
#include <tools/networkTools.h>
|
||||
|
||||
#include <stdio.h>
|
||||
@@ -32,17 +33,30 @@
|
||||
#include <stdexcept>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <iphlpapi.h>
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
#include <iostream>
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <ifaddrs.h>
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <net/if.h>
|
||||
#include <netdb.h>
|
||||
#endif
|
||||
|
||||
#ifdef __HAIKU__
|
||||
#include <sys/sockio.h>
|
||||
#endif
|
||||
|
||||
namespace kiwix
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
size_t write_callback_to_iss(char* ptr, size_t size, size_t nmemb, void* userdata)
|
||||
{
|
||||
auto str = static_cast<std::stringstream*>(userdata);
|
||||
@@ -50,7 +64,15 @@ size_t write_callback_to_iss(char* ptr, size_t size, size_t nmemb, void* userdat
|
||||
return nmemb;
|
||||
}
|
||||
|
||||
std::string kiwix::download(const std::string& url) {
|
||||
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) {
|
||||
auto curl = curl_easy_init();
|
||||
std::stringstream ss;
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
|
||||
@@ -71,103 +93,161 @@ std::string kiwix::download(const std::string& url) {
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
std::map<std::string, std::string> kiwix::getNetworkInterfaces() {
|
||||
std::map<std::string, std::string> interfaces;
|
||||
namespace
|
||||
{
|
||||
|
||||
#ifdef _WIN32
|
||||
SOCKET sd = WSASocket(AF_INET, SOCK_DGRAM, 0, 0, 0, 0);
|
||||
if (sd == INVALID_SOCKET) {
|
||||
std::cerr << "Failed to get a socket. Error " << WSAGetLastError() << std::endl;
|
||||
return interfaces;
|
||||
}
|
||||
|
||||
INTERFACE_INFO InterfaceList[20];
|
||||
unsigned long nBytesReturned;
|
||||
if (WSAIoctl(sd, SIO_GET_INTERFACE_LIST, 0, 0, &InterfaceList,
|
||||
sizeof(InterfaceList), &nBytesReturned, 0, 0) == SOCKET_ERROR) {
|
||||
std::cerr << "Failed calling WSAIoctl: error " << WSAGetLastError() << std::endl;
|
||||
return interfaces;
|
||||
}
|
||||
std::map<std::string, IpAddress> getNetworkInterfacesWin() {
|
||||
std::map<std::string, IpAddress> interfaces;
|
||||
|
||||
int nNumInterfaces = nBytesReturned / sizeof(INTERFACE_INFO);
|
||||
for (int i = 0; i < nNumInterfaces; ++i) {
|
||||
sockaddr_in *pAddress;
|
||||
pAddress = (sockaddr_in *) & (InterfaceList[i].iiAddress.AddressIn);
|
||||
if(pAddress->sin_family == AF_INET) {
|
||||
/* Add to the map */
|
||||
std::string interfaceName = std::string(inet_ntoa(pAddress->sin_addr));
|
||||
interfaces[interfaceName] = interfaceName;
|
||||
}
|
||||
}
|
||||
#else
|
||||
/* Get Network interfaces information */
|
||||
char buf[16384];
|
||||
struct ifconf ifconf;
|
||||
int fd = socket(PF_INET, SOCK_DGRAM, 0); /* Only IPV4 */
|
||||
ifconf.ifc_len = sizeof(buf);
|
||||
ifconf.ifc_buf=buf;
|
||||
if(ioctl(fd, SIOCGIFCONF, &ifconf)!=0) {
|
||||
perror("ioctl(SIOCGIFCONF)");
|
||||
}
|
||||
const int working_buffer_size = 15000;
|
||||
const int max_tries = 3;
|
||||
|
||||
/* Go through each interface */
|
||||
struct ifreq *ifreq;
|
||||
ifreq = ifconf.ifc_req;
|
||||
for (int i = 0; i < ifconf.ifc_len; ) {
|
||||
if (ifreq->ifr_addr.sa_family == AF_INET) {
|
||||
/* Get the network interface ip */
|
||||
char host[128] = { 0 };
|
||||
const int error = getnameinfo(&(ifreq->ifr_addr), sizeof(ifreq->ifr_addr),
|
||||
host, sizeof(host),
|
||||
0, 0, NI_NUMERICHOST);
|
||||
if (!error) {
|
||||
std::string interfaceName = std::string(ifreq->ifr_name);
|
||||
std::string interfaceIp = std::string(host);
|
||||
/* Add to the map */
|
||||
interfaces[interfaceName] = interfaceIp;
|
||||
} else {
|
||||
perror("getnameinfo()");
|
||||
}
|
||||
ULONG flags = GAA_FLAG_INCLUDE_PREFIX;
|
||||
|
||||
// default to unspecified address family (both)
|
||||
ULONG family = AF_UNSPEC;
|
||||
|
||||
ULONG outBufLen = working_buffer_size;
|
||||
ULONG Iterations = 0;
|
||||
DWORD dwRetVal = 0;
|
||||
PIP_ADAPTER_ADDRESSES interfacesHead = NULL;
|
||||
|
||||
// Successively allocate the required memory until GetAdaptersAddresses does not
|
||||
// results in ERROR_BUFFER_OVERFLOW for a maximum of max_tries
|
||||
do{
|
||||
interfacesHead = (IP_ADAPTER_ADDRESSES *) malloc(outBufLen);
|
||||
if (interfacesHead == NULL) {
|
||||
std::cerr << "Memory allocation failed for IP_ADAPTER_ADDRESSES struct" << std::endl;
|
||||
return interfaces;
|
||||
}
|
||||
|
||||
/* some systems have ifr_addr.sa_len and adjust the length that
|
||||
* way, but not mine. weird */
|
||||
size_t len;
|
||||
#ifndef __linux__
|
||||
len = IFNAMSIZ + ifreq->ifr_addr.sa_len;
|
||||
#else
|
||||
len = sizeof(*ifreq);
|
||||
#endif
|
||||
ifreq = (struct ifreq*)((char*)ifreq+len);
|
||||
i += len;
|
||||
dwRetVal = GetAdaptersAddresses(family, flags, NULL, interfacesHead, &outBufLen);
|
||||
} while ((dwRetVal == ERROR_BUFFER_OVERFLOW) && (Iterations < max_tries));
|
||||
|
||||
if (dwRetVal == NO_ERROR) {
|
||||
PIP_ADAPTER_UNICAST_ADDRESS pUnicast = NULL;
|
||||
unsigned int i = 0;
|
||||
for (PIP_ADAPTER_ADDRESSES temp = interfacesHead; temp != NULL;
|
||||
temp = temp->Next) {
|
||||
pUnicast = temp->FirstUnicastAddress;
|
||||
if (pUnicast != NULL) {
|
||||
for (i = 0; pUnicast != NULL; i++){
|
||||
if (pUnicast->Address.lpSockaddr->sa_family == AF_INET)
|
||||
{
|
||||
sockaddr_in *si = (sockaddr_in *)(pUnicast->Address.lpSockaddr);
|
||||
char host[INET_ADDRSTRLEN]={0};
|
||||
inet_ntop(AF_INET, &(si->sin_addr), host, sizeof(host));
|
||||
interfaces[temp->AdapterName].addr=host;
|
||||
}
|
||||
else if (pUnicast->Address.lpSockaddr->sa_family == AF_INET6)
|
||||
{
|
||||
sockaddr_in6 *si = (sockaddr_in6 *)(pUnicast->Address.lpSockaddr);
|
||||
char host[INET6_ADDRSTRLEN]={0};
|
||||
inet_ntop(AF_INET6, &(si->sin6_addr), host, sizeof(host));
|
||||
if (!IN6_IS_ADDR_LINKLOCAL(&(si->sin6_addr)))
|
||||
interfaces[temp->AdapterName].addr6=host;
|
||||
}
|
||||
pUnicast = pUnicast->Next;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
std::cerr << "Call to GetAdaptersAddresses failed with error: "<< dwRetVal << std::endl;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (interfacesHead) free(interfacesHead);
|
||||
|
||||
return interfaces;
|
||||
}
|
||||
|
||||
std::string kiwix::getBestPublicIp() {
|
||||
auto interfaces = getNetworkInterfaces();
|
||||
#else
|
||||
|
||||
std::map<std::string, IpAddress> getNetworkInterfacesPosix() {
|
||||
std::map<std::string, IpAddress> interfaces;
|
||||
|
||||
struct ifaddrs *interfacesHead;
|
||||
if (getifaddrs(&interfacesHead) == -1) {
|
||||
perror("getifaddrs");
|
||||
}
|
||||
|
||||
for (ifaddrs *temp = interfacesHead; temp != NULL; temp = temp->ifa_next) {
|
||||
if (temp->ifa_addr == NULL) continue;
|
||||
|
||||
if (temp->ifa_addr->sa_family == AF_INET) {
|
||||
sockaddr_in *si = (sockaddr_in *)(temp->ifa_addr);
|
||||
char host[INET_ADDRSTRLEN] = {0};
|
||||
inet_ntop(AF_INET, &(si->sin_addr), host, sizeof(host));
|
||||
interfaces[temp->ifa_name].addr=host;
|
||||
} else if (temp->ifa_addr->sa_family == AF_INET6) {
|
||||
sockaddr_in6 *si = (sockaddr_in6 *)(temp->ifa_addr);
|
||||
char host[INET6_ADDRSTRLEN] = {0};
|
||||
inet_ntop(AF_INET6, &(si->sin6_addr), host, sizeof(host));
|
||||
if (!IN6_IS_ADDR_LINKLOCAL(&(si->sin6_addr)))
|
||||
interfaces[temp->ifa_name].addr6=host;
|
||||
}
|
||||
}
|
||||
|
||||
freeifaddrs(interfacesHead);
|
||||
|
||||
return interfaces;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
std::map<std::string, IpAddress> getNetworkInterfacesIPv4Or6() {
|
||||
#ifdef _WIN32
|
||||
return getNetworkInterfacesWin();
|
||||
#else
|
||||
return getNetworkInterfacesPosix();
|
||||
#endif
|
||||
}
|
||||
|
||||
std::map<std::string, std::string> getNetworkInterfaces() {
|
||||
std::map<std::string, std::string> result;
|
||||
for ( const auto& kv : getNetworkInterfacesIPv4Or6() ) {
|
||||
const std::string& interfaceName = kv.first;
|
||||
const auto& ipAddresses = kv.second;
|
||||
if ( !ipAddresses.addr.empty() ) {
|
||||
result[interfaceName] = ipAddresses.addr;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
IpAddress getBestPublicIps() {
|
||||
IpAddress bestPublicIps;
|
||||
std::map<std::string, IpAddress> interfaces = getNetworkInterfacesIPv4Or6();
|
||||
#ifndef _WIN32
|
||||
const char* const prioritizedNames[] =
|
||||
{ "eth0", "eth1", "wlan0", "wlan1", "en0", "en1" };
|
||||
for(auto name: prioritizedNames) {
|
||||
auto it = interfaces.find(name);
|
||||
if(it != interfaces.end()) {
|
||||
return it->second;
|
||||
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) {
|
||||
auto interfaceIp = itr.second;
|
||||
if (interfaceIp.find(prefix) == 0) {
|
||||
return interfaceIp;
|
||||
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 "127.0.0.1";
|
||||
updatePublicIpAddress(bestPublicIps, {"127.0.0.1", "::1"});
|
||||
|
||||
return bestPublicIps;
|
||||
}
|
||||
|
||||
std::string getBestPublicIp()
|
||||
{
|
||||
return getBestPublicIps().addr;
|
||||
}
|
||||
|
||||
} // namespace kiwix
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
#endif
|
||||
|
||||
#include "tools/stringTools.h"
|
||||
#include "server/i18n.h"
|
||||
#include "server/i18n_utils.h"
|
||||
#include "libkiwix-resources.h"
|
||||
|
||||
#include <map>
|
||||
@@ -327,17 +327,27 @@ std::string kiwix::render_template(const std::string& template_str, kainjow::mus
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
// The escapeQuote parameter of escapeForJSON() defaults to true.
|
||||
// This constant makes the calls to escapeForJSON() where the quote symbol
|
||||
// should not be escaped (as it is later replaced with the HTML character entity
|
||||
// ") more readable.
|
||||
static const bool DONT_ESCAPE_QUOTE = false;
|
||||
|
||||
std::string escapeForJSON(const std::string& s)
|
||||
std::string kiwix::escapeForJSON(const std::string& s, bool escapeQuote)
|
||||
{
|
||||
std::ostringstream oss;
|
||||
for (char c : s) {
|
||||
if ( c == '\\' ) {
|
||||
oss << "\\\\";
|
||||
} else if ( unsigned(c) < 0x20U ) {
|
||||
oss << "\\u" << std::setw(4) << std::setfill('0') << unsigned(c);
|
||||
switch ( c ) {
|
||||
case '\n': oss << "\\n"; break;
|
||||
case '\r': oss << "\\r"; break;
|
||||
case '\t': oss << "\\t"; break;
|
||||
default: oss << "\\u" << std::setw(4) << std::setfill('0') << unsigned(c);
|
||||
}
|
||||
} else if ( c == '"' && escapeQuote ) {
|
||||
oss << "\\\"";
|
||||
} else {
|
||||
oss << c;
|
||||
}
|
||||
@@ -345,6 +355,9 @@ std::string escapeForJSON(const std::string& s)
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
std::string makeFulltextSearchSuggestion(const std::string& lang,
|
||||
const std::string& queryString)
|
||||
{
|
||||
@@ -370,10 +383,10 @@ void kiwix::Suggestions::add(const zim::SuggestionItem& suggestion)
|
||||
? suggestion.getSnippet()
|
||||
: suggestion.getTitle();
|
||||
|
||||
result.set("label", escapeForJSON(label));
|
||||
result.set("value", escapeForJSON(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()));
|
||||
result.set("path", escapeForJSON(suggestion.getPath(), DONT_ESCAPE_QUOTE));
|
||||
result.set("first", m_data.is_empty_list());
|
||||
m_data.push_back(result);
|
||||
}
|
||||
@@ -383,8 +396,8 @@ void kiwix::Suggestions::addFTSearchSuggestion(const std::string& uiLang,
|
||||
{
|
||||
kainjow::mustache::data result;
|
||||
const std::string label = makeFulltextSearchSuggestion(uiLang, queryString);
|
||||
result.set("label", escapeForJSON(label));
|
||||
result.set("value", escapeForJSON(queryString + " "));
|
||||
result.set("label", escapeForJSON(label, DONT_ESCAPE_QUOTE));
|
||||
result.set("value", escapeForJSON(queryString + " ", DONT_ESCAPE_QUOTE));
|
||||
result.set("kind", "pattern");
|
||||
result.set("first", m_data.is_empty_list());
|
||||
m_data.push_back(result);
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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, "_");
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ private:
|
||||
const icu::Locale locale;
|
||||
};
|
||||
|
||||
std::string escapeForJSON(const std::string& s, bool escapeQuote = true);
|
||||
|
||||
/* urlEncode() is the equivalent of JS encodeURIComponent(), with the only
|
||||
* difference that the slash (/) symbol is NOT encoded. */
|
||||
@@ -65,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);
|
||||
|
||||
@@ -30,7 +30,10 @@ def get_translation_info(filepath):
|
||||
with open(filepath, 'r', encoding="utf-8") as f:
|
||||
content = json.load(f)
|
||||
lang_name = content.get("name")
|
||||
return lang_code, lang_name
|
||||
translation_count = len(content)
|
||||
return dict(iso_code=lang_code,
|
||||
self_name=lang_name,
|
||||
translation_count=translation_count)
|
||||
|
||||
language_list = []
|
||||
json_files = translation_dir.glob("*.json")
|
||||
@@ -40,14 +43,14 @@ with open(resource_file, 'w', encoding="utf-8") as f:
|
||||
continue
|
||||
print("Processing", i18n_file.name)
|
||||
if i18n_file.name != "test.json":
|
||||
lang_code, lang_name = get_translation_info(i18n_file)
|
||||
translation_info = get_translation_info(i18n_file)
|
||||
lang_name = translation_info["self_name"]
|
||||
if lang_name:
|
||||
language_list.append((lang_code, lang_name))
|
||||
language_list.append(translation_info)
|
||||
else:
|
||||
print(f"Warning: missing 'name' in {i18n_file.name}")
|
||||
f.write(str(i18n_file.relative_to(script_path.parent)) + '\n')
|
||||
|
||||
language_list = [{name: code} for code, name in sorted(language_list)]
|
||||
language_list_jsobj_str = json.dumps(language_list,
|
||||
indent=2,
|
||||
ensure_ascii=False)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
skin/i18n/ar.json
|
||||
skin/i18n/bn.json
|
||||
skin/i18n/br.json
|
||||
skin/i18n/cs.json
|
||||
skin/i18n/dag.json
|
||||
skin/i18n/de.json
|
||||
skin/i18n/dga.json
|
||||
skin/i18n/el.json
|
||||
@@ -8,10 +10,14 @@ skin/i18n/en.json
|
||||
skin/i18n/es.json
|
||||
skin/i18n/fi.json
|
||||
skin/i18n/fr.json
|
||||
skin/i18n/ha.json
|
||||
skin/i18n/he.json
|
||||
skin/i18n/hi.json
|
||||
skin/i18n/hu.json
|
||||
skin/i18n/hy.json
|
||||
skin/i18n/ia.json
|
||||
skin/i18n/id.json
|
||||
skin/i18n/ig.json
|
||||
skin/i18n/it.json
|
||||
skin/i18n/ja.json
|
||||
skin/i18n/ko.json
|
||||
@@ -19,10 +25,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
|
||||
@@ -30,6 +40,7 @@ skin/i18n/skr-arab.json
|
||||
skin/i18n/sl.json
|
||||
skin/i18n/sq.json
|
||||
skin/i18n/sv.json
|
||||
skin/i18n/sw.json
|
||||
skin/i18n/te.json
|
||||
skin/i18n/test.json
|
||||
skin/i18n/tr.json
|
||||
|
||||
@@ -1,22 +1,30 @@
|
||||
skin/caret.png
|
||||
skin/bittorrent.png
|
||||
skin/magnet.png
|
||||
skin/404.svg
|
||||
skin/500.svg
|
||||
skin/blocklink.svg
|
||||
skin/feed.svg
|
||||
skin/langSelector.svg
|
||||
skin/download.png
|
||||
skin/download-white.svg
|
||||
skin/hash.png
|
||||
skin/search-icon.svg
|
||||
skin/iso6391To3.js
|
||||
skin/isotope.pkgd.min.js
|
||||
skin/index.js
|
||||
skin/autoComplete/autoComplete.min.js
|
||||
skin/error.css
|
||||
skin/print.css
|
||||
skin/kiwix.css
|
||||
skin/taskbar.css
|
||||
skin/index.css
|
||||
skin/fonts/DMSans-Regular.ttf
|
||||
skin/fonts/Poppins.ttf
|
||||
skin/fonts/Roboto.ttf
|
||||
skin/search_results.css
|
||||
skin/blank.html
|
||||
skin/polyfills.js
|
||||
skin/viewer.js
|
||||
skin/i18n.js
|
||||
skin/languages.js
|
||||
@@ -36,10 +44,12 @@ 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
|
||||
templates/sexy500.html
|
||||
opensearchdescription.xml
|
||||
ft_opensearchdescription.xml
|
||||
catalog_v2_searchdescription.xml
|
||||
|
||||
1
static/skin/404.svg
Normal file
1
static/skin/404.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 7.4 KiB |
1
static/skin/500.svg
Normal file
1
static/skin/500.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 9.7 KiB |
@@ -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);
|
||||
|
||||
1
static/skin/blocklink.svg
Normal file
1
static/skin/blocklink.svg
Normal 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 |
6
static/skin/download-white.svg
Normal file
6
static/skin/download-white.svg
Normal 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
159
static/skin/error.css
Normal 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;
|
||||
}
|
||||
}
|
||||
BIN
static/skin/fonts/DMSans-Regular.ttf
Normal file
BIN
static/skin/fonts/DMSans-Regular.ttf
Normal file
Binary file not shown.
@@ -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 {
|
||||
@@ -69,15 +70,66 @@ function $t(msgId, params={}) {
|
||||
}
|
||||
}
|
||||
|
||||
const I18n = {
|
||||
instantiateParameterizedMessages: function(data) {
|
||||
if ( data.__proto__ == Array.prototype ) {
|
||||
const result = [];
|
||||
for ( const x of data ) {
|
||||
result.push(this.instantiateParameterizedMessages(x));
|
||||
}
|
||||
return result;
|
||||
} else if ( data.__proto__ == Object.prototype ) {
|
||||
const msgId = data.msgid;
|
||||
const msgParams = data.params;
|
||||
if ( msgId && msgId.__proto__ == String.prototype && msgParams && msgParams.__proto__ == Object.prototype ) {
|
||||
return $t(msgId, msgParams);
|
||||
} else {
|
||||
const result = {};
|
||||
for ( const p in data ) {
|
||||
result[p] = this.instantiateParameterizedMessages(data[p]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
} else {
|
||||
return data;
|
||||
}
|
||||
},
|
||||
|
||||
render: function (template, params) {
|
||||
params = this.instantiateParameterizedMessages(params);
|
||||
return mustache.render(template, params);
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_UI_LANGUAGE = 'en';
|
||||
|
||||
Translations.load(DEFAULT_UI_LANGUAGE, /*asDefault=*/true);
|
||||
|
||||
// Below function selects the most suitable UI language from the list
|
||||
// of preferred languages in browser preferences and available translations.
|
||||
// Since, unlike Accept-Language header, navigator.languages doesn't contain
|
||||
// qvalues, they are computed using the same algorithm as in Firefox 121
|
||||
function getDefaultUserLanguage() {
|
||||
const mostSuitableLang = { code: DEFAULT_UI_LANGUAGE, score: 0 }
|
||||
const n = navigator.languages.length;
|
||||
for (const lang of uiLanguages ) {
|
||||
const rank = navigator.languages.indexOf(lang.iso_code);
|
||||
if ( rank >= 0 ) {
|
||||
const qvalue = Math.round(10*(1 - rank/n))/10;
|
||||
const score = qvalue * lang.translation_count;
|
||||
if ( score > mostSuitableLang.score ) {
|
||||
mostSuitableLang.code = lang.iso_code;
|
||||
mostSuitableLang.score = score;
|
||||
}
|
||||
}
|
||||
}
|
||||
return mostSuitableLang.code;
|
||||
}
|
||||
|
||||
function getUserLanguage() {
|
||||
return new URLSearchParams(window.location.search).get('userlang')
|
||||
|| window.localStorage.getItem('userlang')
|
||||
|| viewerSettings.defaultUserLanguage
|
||||
|| DEFAULT_UI_LANGUAGE;
|
||||
|| getDefaultUserLanguage();
|
||||
}
|
||||
|
||||
function setUserLanguage(lang, callback) {
|
||||
@@ -133,15 +185,46 @@ function initUILanguageSelector(activeLanguage, languageChangeCallback) {
|
||||
}
|
||||
const languageSelector = document.getElementById("ui_language");
|
||||
for (const lang of uiLanguages ) {
|
||||
const lang_name = Object.getOwnPropertyNames(lang)[0];
|
||||
const lang_code = lang[lang_name];
|
||||
const is_selected = lang_code == activeLanguage;
|
||||
languageSelector.appendChild(new Option(lang_name, lang_code, is_selected, is_selected));
|
||||
const is_selected = lang.iso_code == activeLanguage;
|
||||
languageSelector.appendChild(new Option(lang.self_name, lang.iso_code, is_selected, is_selected));
|
||||
}
|
||||
languageSelector.onchange = languageChangeCallback;
|
||||
}
|
||||
|
||||
function parseDom(html) {
|
||||
const domParser = new DOMParser();
|
||||
return domParser.parseFromString(html, "text/html").documentElement;
|
||||
}
|
||||
|
||||
function translatePageInWindow(w) {
|
||||
if ( w.KIWIX_RESPONSE_TEMPLATE && w.KIWIX_RESPONSE_DATA ) {
|
||||
const template = parseDom(w.KIWIX_RESPONSE_TEMPLATE).textContent;
|
||||
|
||||
// w.KIWIX_RESPONSE_DATA may belong to a different context and running
|
||||
// I18n.render() on it directly won't work correctly
|
||||
// because the type checks (obj.__proto__ == ???.prototype) in
|
||||
// I18n.instantiateParameterizedMessages() will fail (String.prototype
|
||||
// refers to different objects in different contexts).
|
||||
// Work arround that issue by copying the object into our context.
|
||||
const params = JSON.parse(JSON.stringify(w.KIWIX_RESPONSE_DATA));
|
||||
|
||||
const newHtml = I18n.render(template, params);
|
||||
w.document.documentElement.innerHTML = parseDom(newHtml).innerHTML;
|
||||
}
|
||||
}
|
||||
|
||||
function translateSelf() {
|
||||
if ( window.KIWIX_RESPONSE_TEMPLATE && window.KIWIX_RESPONSE_DATA ) {
|
||||
setUserLanguage(getUserLanguage(), () => {
|
||||
translatePageInWindow(window)
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
window.$t = $t;
|
||||
window.getUserLanguage = getUserLanguage;
|
||||
window.setUserLanguage = setUserLanguage;
|
||||
window.initUILanguageSelector = initUILanguageSelector;
|
||||
window.translatePageInWindow = translatePageInWindow;
|
||||
window.I18n = I18n;
|
||||
window.addEventListener('load', translateSelf);
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Asma",
|
||||
"Hamoudak",
|
||||
"Meno25",
|
||||
"Mohammed Qays",
|
||||
"Ravan",
|
||||
"محمد أحمد عبد الفتاح"
|
||||
]
|
||||
@@ -12,7 +15,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 +24,32 @@
|
||||
"400-page-heading": "طلب غير صالح",
|
||||
"404-page-title": "المحتوى غير موجود",
|
||||
"404-page-heading": "لم يتم العثور عليه",
|
||||
"500-page-title": "خطأ في الخادم الداخلي",
|
||||
"500-page-heading": "خطأ في الخادم الداخلي",
|
||||
"fulltext-search-unavailable": "البحث عن النص الكامل غير متاح",
|
||||
"no-search-results": "محرك البحث عن النص الكامل غير متاح لهذا المحتوى.",
|
||||
"library-button-text": "اذهب لصفحة الترحيب",
|
||||
"home-button-text": "انتقل إلى الصفحة الرئيسية لـ \"{{BOOK_TITLE}}\"",
|
||||
"500-page-title": "خطأ داخلي بالخادم",
|
||||
"500-page-heading": "عفواً، الصفحة لا تعمل.",
|
||||
"500-page-text": "لا يمكن إرسال المسار المطلوب بشكل صحيح:",
|
||||
"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": "قد يشارك في البحث كتابان أو أكثر بلغات مختلفة، مما قد يؤدي إلى نتائج محيرة."
|
||||
"searchbox-tooltip": "ابحث عن '{{{BOOK_TITLE}}}'",
|
||||
"confusion-of-tongues": "قد يشارك في البحث كتابان أو أكثر بلغات مختلفة، مما قد يؤدي إلى نتائج محيرة.",
|
||||
"direct-download-alt-text": "التنزيل مباشرة عبر بروتوكول HTTP(S)",
|
||||
"hash-download-link-text": "المجموع الإختباري لخوارزمية SHA-256",
|
||||
"hash-download-alt-text": "عرض المجموع الإختباري لخوارزمية SHA-256 للملف",
|
||||
"magnet-link-text": "رابط جاذب",
|
||||
"magnet-alt-text": "التنزيل بواسطة الرابط الجاذب",
|
||||
"torrent-download-link-text": "بت تورنت",
|
||||
"torrent-download-alt-text": "التنزيل بواسطة بت تورنت",
|
||||
"unknown-error": "خطأ غير معروف",
|
||||
"book-category.wikibooks": "ويكي الكتب",
|
||||
"book-category.wikinews": "ويكي الأخبار",
|
||||
"book-category.wikipedia": "ويكيبيديا",
|
||||
"book-category.wikiquote": "ويكي الإقتباس",
|
||||
"book-category.wikisource": "ويكي مصدر",
|
||||
"book-category.wikispecies": "ويكي أنواع",
|
||||
"book-category.wikiversity": "ويكي الجامعة",
|
||||
"book-category.wikivoyage": "ويكي الرحلات",
|
||||
"book-category.wiktionary": "ويكي القاموس",
|
||||
"book-category.other": "أخرى {كتب أخري}"
|
||||
}
|
||||
|
||||
@@ -1,18 +1,41 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Nokib Sarkar",
|
||||
"আফতাবুজ্জামান"
|
||||
]
|
||||
},
|
||||
"name": "বাংলা",
|
||||
"404-page-heading": "পাওয়া যায়নি",
|
||||
"new-404-page-title": "পাতা পাওয়া যায়নি",
|
||||
"new-404-page-heading": "ওহ! পাতা খুঁজে পাওয়া যায়নি",
|
||||
"404-img-text": "পাওয়া যায়নি!",
|
||||
"path-was-not-found": "অনুরোধ করা পথটি পাওয়া যায়নি:",
|
||||
"404-advice.p1": "আপনি যে বিষয়বস্তু খুঁজছেন তা এখনও উপলব্ধ থাকতে পারে, তবে এটি জিম ফাইলের মধ্যে অন্য কোনও জায়গায় অবস্থিত হতে পারে।",
|
||||
"404-advice.p4": "আপনি যে তথ্য খুঁজছেন তার সাথে সম্পর্কিত কীওয়ার্ড বা শিরোনাম খুঁজুন।",
|
||||
"500-page-title": "অভ্যন্তরীণ সার্ভার ত্রুটি",
|
||||
"500-page-heading": "অভ্যন্তরীণ সার্ভার ত্রুটি",
|
||||
"external-link-intro": "আপনি অনলাইনে যাওয়ার জন্য কিউইক্সের জিম রিডার ছেড়ে দিতে চলেছেন",
|
||||
"external-link-advice.p1": "আপনি যে লিঙ্কটিতে প্রবেশ করার চেষ্টা করছেন তা আপনার অফলাইন প্যাকেজের অংশ নয় এবং এর জন্য ইন্টারনেট সংযোগ প্রয়োজন।",
|
||||
"external-link-advice.p2": "আপনি যদি অনলাইনে যেতে পারেন, তাহলে লিঙ্কটি খোলার চেষ্টা করতে পারেন।",
|
||||
"external-link-advice.p3": "অন্যথায়, আপনি আপনার ব্রাউজারের পিছন বোতাম ব্যবহার করে আপনার ZIM-এর অফলাইন বিষয়বস্তুতে ফিরে যেতে পারেন।",
|
||||
"search-result-book-info": "{{BOOK_TITLE}} থেকে",
|
||||
"word-count": "{{COUNT}}টি শব্দ",
|
||||
"library-button-text": "স্বাগত পাতায় চলুন",
|
||||
"home-button-text": "'{{BOOK_TITLE}}'-এর প্রধান পাতায় চলুন",
|
||||
"searchbox-tooltip": "'{{BOOK_TITLE}}' অনুসন্ধান করুন",
|
||||
"search": "অনুসন্ধান",
|
||||
"welcome-to-kiwix-server": "কিউইক্স সার্ভারে স্বাগতম",
|
||||
"download-links-title": "বই ডাউনলোড করুন",
|
||||
"preview-book": "প্রাকদর্শন"
|
||||
"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": "অন্যান্য"
|
||||
}
|
||||
|
||||
42
static/skin/i18n/br.json
Normal file
42
static/skin/i18n/br.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Adriendelucca",
|
||||
"Y-M D"
|
||||
]
|
||||
},
|
||||
"name": "brezhoneg",
|
||||
"suggest-full-text-search": "E lec'h emañ \"{{{SEARCH_TERMS}}}\"...",
|
||||
"no-such-book": "N’eus ket eus al levr-mañ: {{BOOK_NAME}}",
|
||||
"no-book-found": "N’eus levr ebet a glot gant an dezverkoù-se",
|
||||
"url-not-found": "N’eo ket bet kavet an URL \"{{url}}\" goulennet war ar servijer-mañ.",
|
||||
"random-article-failure": "Chaous! N’hon eus ket gellet dibab ur pennad dre ziouer evidoc’h :(",
|
||||
"400-page-title": "Reked amwiriek",
|
||||
"400-page-heading": "Reked amwiriek",
|
||||
"404-page-heading": "N'eo ket bet kavet",
|
||||
"500-page-title": "Fazi diabarzh ar servijer",
|
||||
"500-page-heading": "Fazi diabarzh ar servijer",
|
||||
"search-results-page-title": "Klask: {{SEARCH_PATTERN}}",
|
||||
"search-results-page-header": "Disoc’hoù <b>{{START}}-{{END}}</b> diwar <b>{{COUNT}}</b> evit <b>\"{{{SEARCH_PATTERN}}}\"</b>",
|
||||
"empty-search-results-page-header": "Disoc’h ebet kavet evit <b>\"{{{SEARCH_PATTERN}}}\"</b>",
|
||||
"search-result-book-info": "diouzh {{BOOK_TITLE}}",
|
||||
"word-count": "{{COUNT}} a c’herioù",
|
||||
"library-button-text": "Mont d’ar bajenn degemer",
|
||||
"home-button-text": "Mont da bajenn degemer \"{{BOOK_TITLE}}\"",
|
||||
"random-page-button-text": "Mont d’ur bajenn dre zegouezh",
|
||||
"searchbox-tooltip": "Klask '{{BOOK_TITLE}}'",
|
||||
"powered-by-kiwix-html": "Lusket gant <a href=\"https://kiwix.org\">Kiwix</a>",
|
||||
"search": "Klask",
|
||||
"book-filtering-all-categories": "An holl rummadoù",
|
||||
"book-filtering-all-languages": "An holl yezhoù",
|
||||
"count-of-matching-books": "{{COUNT}} levr",
|
||||
"download": "Pellgargañ",
|
||||
"direct-download-link-text": "Eeun",
|
||||
"filter-by-tag": "Silañ gant an dikedenn \"{{TAG}}\"",
|
||||
"stop-filtering-by-tag": "Paouez da silañ gant an dikedenn \"{{TAG}}\"",
|
||||
"welcome-to-kiwix-server": "Degemer mat er servijer Kiwix",
|
||||
"download-links-heading": "Liammoù pellgargañ evit <b><i>{{BOOK_TITLE}}</i></b>",
|
||||
"download-links-title": "Pellgargañ al levr",
|
||||
"preview-book": "Rakwelet",
|
||||
"unknown-error": "Fazi dianav"
|
||||
}
|
||||
55
static/skin/i18n/dag.json
Normal file
55
static/skin/i18n/dag.json
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Kalakpagh",
|
||||
"Ruky Wunpini"
|
||||
]
|
||||
},
|
||||
"name": "Silimiinsili",
|
||||
"suggest-full-text-search": "Gbubi la '{{{SEARCH_TERMS}}}'...",
|
||||
"no-such-book": "Lala buku kani:{{BOOK_NAME}}",
|
||||
"too-many-books": "Buku nima pam ka bɛ daa suhi ({{NB_BOOKS}}) din ni ka tariga nyɛ {{LIMIT}}",
|
||||
"no-book-found": "Buku kani lu zahim a ni piigi yaɣa shɛli",
|
||||
"url-not-found": "URL \"{{url}}\" shɛli bɛ ni daa suhi daa kani n-ti tum tumda ŋɔ.",
|
||||
"suggest-search": "Niŋmi lahabali pali vihigu zaŋ n-ti <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
|
||||
"random-article-failure": "Oops! Zaɣisiya ni di gahim piigi lahabali :(",
|
||||
"invalid-raw-data-type": "{{DATATYPE}} nyɛla din suhibu bi niŋ viɛnyɛla zaŋ n-ti lahabali kahili.",
|
||||
"invalid-request": "URL \"{{{url}}}\" shɛli bɛ ni daa suhi ŋɔ nyɛla din bi suhi viɛnyɛla.",
|
||||
"no-value-for-arg": "Dariza shɛli bi ti zaŋ n-ti nangban'kpeeni {{ARGUMENT}}",
|
||||
"no-query": "Yɛlshɛli bi yiina",
|
||||
"raw-entry-not-found": "Ku tooi nya {{DATATYPE}} kpɛbu {{ENTRY}}",
|
||||
"400-page-title": "Suhigu din bi niŋ viɛnyɛla",
|
||||
"400-page-heading": "Suhigu din bi niŋ viɛnyɛla",
|
||||
"404-page-title": "Lahabali kani",
|
||||
"404-page-heading": "Kani",
|
||||
"500-page-title": "Puuni tum tumda chiriŋ",
|
||||
"500-page-heading": "Puuni tum tumda chiriŋ",
|
||||
"500-page-text": "Puuni tum tumda chiriŋ niŋya. Ti niŋ yolitem zaŋ jɛndi li :/",
|
||||
"fulltext-search-unavailable": "Lahabali pali vihigu kani",
|
||||
"search-results-page-title": "Vihima:{{SEARCH_PATTERN}}",
|
||||
"search-results-page-header": "Chaɣili nima <b>{{START}}-{{END}}</b> of <b>{{COUNT}}</b> for <b>\"{{{SEARCH_PATTERN}}}\"</b>",
|
||||
"empty-search-results-page-header": "Chaɣili daa kani zaŋ n-ti\n <b>\"{{{SEARCH_PATTERN}}}\"</b>",
|
||||
"search-result-book-info": "yina {{BOOK_TITLE}}",
|
||||
"word-count": "{{COUNT}} bachi nima",
|
||||
"library-button-text": "Cham solɔɣu",
|
||||
"home-button-text": "Cham yaɣili maŋmaŋ zaŋ n-ti\n'{{BOOK_TITLE}}'",
|
||||
"random-page-button-text": "Cham gahim piigi yaɣili",
|
||||
"searchbox-tooltip": "Vihima '{{BOOK_TITLE}}'",
|
||||
"confusion-of-tongues": "Buku nima ayi bee gari balli koŋkoba nyɛ din yɛn be vihigu ŋɔ ni ka di ni tooi chɛ ka di laasabu wali.",
|
||||
"welcome-page-overzealous-filter": "Labisibu kani. A ni yu ni a\n<a href=\"{{URL}}\">reset filter</a>?",
|
||||
"powered-by-kiwix-html": "Din niŋ li nyɛ <a href=\"https://kiwix.org\">Kiwix</a>",
|
||||
"search": "Vihima",
|
||||
"book-filtering-all-categories": "Pubu zaa",
|
||||
"book-filtering-all-languages": "Bala zaa",
|
||||
"count-of-matching-books": "{{COUNT}} Buku(nima)",
|
||||
"download": "Deebu",
|
||||
"direct-download-link-text": "Tibi",
|
||||
"direct-download-alt-text": "Tibi deebu",
|
||||
"hash-download-link-text": "Sha256 hash",
|
||||
"hash-download-alt-text": "Deebu daliŋ",
|
||||
"welcome-to-kiwix-server": "Maraba Kiwix tum tumda",
|
||||
"download-links-heading": "Deemi soli zaŋ n-ti <b><i>{{BOOK_TITLE}}</i></b>",
|
||||
"download-links-title": "Deemi buku",
|
||||
"preview-book": "Daŋyuli",
|
||||
"unknown-error": "Chiriŋ din bi tooi baŋ"
|
||||
}
|
||||
@@ -1,8 +1,12 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"AnnaTheProgrammer777",
|
||||
"IMayBeABitShy",
|
||||
"Justman10000",
|
||||
"Lucas Werkmeister",
|
||||
"MoritzMT20",
|
||||
"Rofiatmustapha12",
|
||||
"ThisCarthing"
|
||||
]
|
||||
},
|
||||
@@ -15,6 +19,7 @@
|
||||
"suggest-search": "Führe eine Volltextsuche nach <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a> durch",
|
||||
"random-article-failure": "Hoppla! Konnte keinen zufälligen Artikel auswählen :(",
|
||||
"invalid-raw-data-type": "{{DATATYPE}} ist keine gültige Anfrage für unverarbeiteten Inhalt",
|
||||
"invalid-request": "Die angeforderte URL „{{{url}}}“ ist keine gültige Anfrage.",
|
||||
"no-value-for-arg": "Kein Wert für den Parameter {{ARGUMENT}} gegeben",
|
||||
"no-query": "Keine Suchanfrage gegeben.",
|
||||
"raw-entry-not-found": "Eintrag {{ENTRY}} des Typs {{DATATYPE}} konnte nicht gefunden werden.",
|
||||
@@ -24,12 +29,18 @@
|
||||
"404-page-heading": "Nicht gefunden",
|
||||
"500-page-title": "Interner Server-Fehler",
|
||||
"500-page-heading": "Interner Server-Fehler",
|
||||
"500-page-text": "Es ist ein interner Serverfehler aufgetreten. Das tut uns leid :/",
|
||||
"fulltext-search-unavailable": "Die Volltestsuche steht nicht zur Verfügung.",
|
||||
"no-search-results": "Die Volltextsuche ist für diesen Inhalt nicht verfügbar.",
|
||||
"search-results-page-title": "Suche: {{SEARCH_PATTERN}}",
|
||||
"search-results-page-header": "Ergebnisse <b>{{START}}-{{END}}</b> von <b>{{COUNT}}</b> für <b>„{{{SEARCH_PATTERN}}}“</b>",
|
||||
"empty-search-results-page-header": "Für <b>„{{{SEARCH_PATTERN}}}“</b> wurden keine Ergebnisse gefunden.",
|
||||
"search-result-book-info": "von {{BOOK_TITLE}}",
|
||||
"word-count": "{{COUNT}} Wörter",
|
||||
"library-button-text": "Zur Willkommensseite gehen",
|
||||
"home-button-text": "Zur Hauptseite von '{{BOOK_TITLE}}' gehen",
|
||||
"home-button-text": "Zur Hauptseite von '{{{BOOK_TITLE}}}' gehen",
|
||||
"random-page-button-text": "Zu einer zufällig ausgewählten Seite gehen",
|
||||
"searchbox-tooltip": "Nach '{{BOOK_TITLE}}' suchen",
|
||||
"searchbox-tooltip": "'{{{BOOK_TITLE}}}' durchsuchen",
|
||||
"confusion-of-tongues": "Zwei oder mehr Bücher unterschiedlicher Sprachen werden durchsucht, was zu unübersichtlichen Ergebnissen führen kann.",
|
||||
"welcome-page-overzealous-filter": "Keine Ergebnisse gefunden. Möchten Sie den <a href=\"{{URL}}\">Filter zurücksetzen</a>?",
|
||||
"powered-by-kiwix-html": "Angetrieben durch <a href=\"https://kiwix.org\">Kiwix</a>",
|
||||
@@ -39,19 +50,30 @@
|
||||
"count-of-matching-books": "{{COUNT}} Bücher",
|
||||
"download": "Herunterladen",
|
||||
"direct-download-link-text": "Direkt",
|
||||
"direct-download-alt-text": "direkt herunterladen",
|
||||
"hash-download-link-text": "Sha256 Hash",
|
||||
"hash-download-alt-text": "Hash herunterladen",
|
||||
"direct-download-alt-text": "Direktes Herunterladen über HTTP(S)",
|
||||
"hash-download-link-text": "SHA-256 Prüfsumme",
|
||||
"hash-download-alt-text": "SHA-256-Dateiprüfsumme anzeigen",
|
||||
"magnet-link-text": "Magnet Link",
|
||||
"magnet-alt-text": "Magnet Link herunterladen",
|
||||
"torrent-download-link-text": "Torrent-Datei",
|
||||
"torrent-download-alt-text": "Torrent herunterladen",
|
||||
"magnet-alt-text": "Download über Magnet-Link",
|
||||
"torrent-download-link-text": "BitTorrent",
|
||||
"torrent-download-alt-text": "Herunterladen über BitTorrent",
|
||||
"library-opds-feed-all-entries": "ODPS Feed der Bibliothek - Alle Einträge",
|
||||
"filter-by-tag": "Nach Tag \"{{TAG}}\" filtern",
|
||||
"stop-filtering-by-tag": "Filterung nach Tag \"{{TAG}}\" aufheben",
|
||||
"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"
|
||||
"preview-book": "Vorschau",
|
||||
"unknown-error": "Unbekannter Fehler",
|
||||
"book-category.wikibooks": "Wikibooks",
|
||||
"book-category.wikinews": "Wikinews",
|
||||
"book-category.wikipedia": "Wikipedia",
|
||||
"book-category.wikiquote": "Wikiquote",
|
||||
"book-category.wikisource": "Wikisource",
|
||||
"book-category.wikispecies": "Wikispecies",
|
||||
"book-category.wikiversity": "Wikiversity",
|
||||
"book-category.wikivoyage": "Wikivoyage",
|
||||
"book-category.wiktionary": "Wiktionary",
|
||||
"book-category.other": "Andere"
|
||||
}
|
||||
|
||||
@@ -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>?",
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Jimkats",
|
||||
"Kelson",
|
||||
"Norhorn"
|
||||
"Norhorn",
|
||||
"Ανώνυμος Βικιπαιδιστής"
|
||||
]
|
||||
},
|
||||
"name": "Αγγλικά",
|
||||
"suggest-full-text-search": "περιέχει '{{{SEARCH_TERMS}}}'...",
|
||||
"no-such-book": "Δεν υπάρχει τέτοιο βιβλίο: {{BOOK_NAME}}",
|
||||
"caution-warning": "Προσοχή!",
|
||||
"search-result-book-info": "από {{BOOK_TITLE}}",
|
||||
"word-count": "{{COUNT}} λέξεις",
|
||||
"welcome-page-overzealous-filter": "Κανένα αποτέλεσμα. Θέλετε να <a href=\"{{URL}}\">επαναφέρετε το φίλτρο</a>;",
|
||||
"powered-by-kiwix-html": "Με την υποστήριξη by <a href=\"https://kiwix.org\">Kiwix</a>",
|
||||
"search": "Αναζήτηση",
|
||||
@@ -15,8 +23,14 @@
|
||||
"direct-download-link-text": "Απευθείας",
|
||||
"direct-download-alt-text": "άμεση λήψη",
|
||||
"hash-download-alt-text": "λήψη αναγνωριστικού",
|
||||
"torrent-download-link-text": "Αρχείο torrent",
|
||||
"torrent-download-alt-text": "λήψη torrent",
|
||||
"filter-by-tag": "Φίλτρο ανά ετικέτα \"{{TAG}}\"",
|
||||
"stop-filtering-by-tag": "Διακοπή φίλτρου ανά ετικέτα \"{{TAG}}\""
|
||||
"magnet-alt-text": "λήψη μαγνήτη",
|
||||
"torrent-download-link-text": "BitTorrent",
|
||||
"torrent-download-alt-text": "Λήψη μέσω BitTorrent",
|
||||
"filter-by-tag": "Φιλτράρισμα κατά ετικέτα \"{{{TAG}}}\"",
|
||||
"stop-filtering-by-tag": "Διακοπή φιλτραρίσματος κατά ετικέτα \"{{{TAG}}}\"",
|
||||
"welcome-to-kiwix-server": "Καλώς ορίσατε στον διακομιστή Kiwix",
|
||||
"download-links-heading": "Λήψη συνδέσμων για <b><i>{{BOOK_TITLE}}</i></b>",
|
||||
"download-links-title": "Κατεβάστε το βιβλίο",
|
||||
"preview-book": "Προεπισκόπηση",
|
||||
"unknown-error": "Άγνωστο σφάλμα"
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
, "suggest-search" : "Make a full text search for <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>"
|
||||
, "random-article-failure" : "Oops! Failed to pick a random article :("
|
||||
, "invalid-raw-data-type" : "{{DATATYPE}} is not a valid request for raw content."
|
||||
, "invalid-request" : "The requested URL \"{{{url}}}\" is not a valid request."
|
||||
, "no-value-for-arg": "No value provided for argument {{ARGUMENT}}"
|
||||
, "no-query" : "No query provided."
|
||||
, "raw-entry-not-found" : "Cannot find {{DATATYPE}} entry {{ENTRY}}"
|
||||
@@ -19,14 +20,36 @@
|
||||
, "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-heading" : "Oops. Page isn't working."
|
||||
, "500-page-text": "The requested path cannot be properly delivered:"
|
||||
, "500-img-text": "Page isn't working"
|
||||
, "external-link-detected" : "External Link Detected"
|
||||
, "caution-warning" : "Caution!"
|
||||
, "external-link-intro" : "You are about to leave Kiwix's ZIM reader to go online to"
|
||||
, "external-link-advice.p1": "The link you're trying to access is not part of your offline package and requires an internet connection."
|
||||
, "external-link-advice.p2": "If you can go online, you can attempt to open the link."
|
||||
, "external-link-advice.p3": "You can otherwise return to your ZIM's offline content by using your browser's back button."
|
||||
, "fulltext-search-unavailable" : "Fulltext search unavailable"
|
||||
, "no-search-results": "The fulltext search engine is not available for this content."
|
||||
, "search-results-page-title": "Search: {{SEARCH_PATTERN}}"
|
||||
, "search-results-page-header": "Results <b>{{START}}-{{END}}</b> of <b>{{COUNT}}</b> for <b>\"{{{SEARCH_PATTERN}}}\"</b>"
|
||||
, "empty-search-results-page-header": "No results were found for <b>\"{{{SEARCH_PATTERN}}}\"</b>"
|
||||
, "search-result-book-info": "from {{BOOK_TITLE}}"
|
||||
, "word-count": "{{COUNT}} words"
|
||||
, "library-button-text": "Go to welcome page"
|
||||
, "home-button-text": "Go to the main page of '{{BOOK_TITLE}}'"
|
||||
, "home-button-text": "Go to the main page of '{{{BOOK_TITLE}}}'"
|
||||
, "random-page-button-text": "Go to a randomly selected page"
|
||||
, "searchbox-tooltip": "Search '{{BOOK_TITLE}}'"
|
||||
, "searchbox-tooltip": "Search '{{{BOOK_TITLE}}}'"
|
||||
, "confusion-of-tongues": "Two or more books in different languages would participate in search, which may lead to confusing results."
|
||||
, "welcome-page-overzealous-filter": "No result. Would you like to <a href=\"{{URL}}\">reset filter</a>?"
|
||||
, "powered-by-kiwix-html": "Powered by <a href=\"https://kiwix.org\">Kiwix</a>"
|
||||
@@ -36,19 +59,40 @@
|
||||
, "count-of-matching-books": "{{COUNT}} book(s)"
|
||||
, "download": "Download"
|
||||
, "direct-download-link-text": "Direct"
|
||||
, "direct-download-alt-text": "direct download"
|
||||
, "hash-download-link-text": "Sha256 hash"
|
||||
, "hash-download-alt-text": "download hash"
|
||||
, "direct-download-alt-text": "Download directly via HTTP(S)"
|
||||
, "hash-download-link-text": "SHA-256 checksum"
|
||||
, "hash-download-alt-text": "Display SHA-256 file checksum"
|
||||
, "magnet-link-text": "Magnet link"
|
||||
, "magnet-alt-text": "download magnet"
|
||||
, "torrent-download-link-text": "Torrent file"
|
||||
, "torrent-download-alt-text": "download torrent"
|
||||
, "magnet-alt-text": "Download via Magnet link"
|
||||
, "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>"
|
||||
, "download-links-title": "Download book"
|
||||
, "preview-book": "Preview"
|
||||
, "non-translated-text": "{{MSG}}"
|
||||
, "unknown-error": "Unknown error"
|
||||
, "book-category.gutenberg": "Gutenberg"
|
||||
, "book-category.iFixit": "iFixit"
|
||||
, "book-category.mooc": "MOOC"
|
||||
, "book-category.phet": "Phet"
|
||||
, "book-category.stack_exchange": "Stack Exchange"
|
||||
, "book-category.ted": "Ted"
|
||||
, "book-category.vikidia": "Vikidia"
|
||||
, "book-category.wikibooks": "Wikibooks"
|
||||
, "book-category.wikihow": "wikiHow"
|
||||
, "book-category.wikinews": "Wikinews"
|
||||
, "book-category.wikipedia": "Wikipedia"
|
||||
, "book-category.wikiquote": "Wikiquote"
|
||||
, "book-category.wikisource": "Wikisource"
|
||||
, "book-category.wikispecies": "Wikispecies"
|
||||
, "book-category.wikiversity": "Wikiversity"
|
||||
, "book-category.wikivoyage": "Wikivoyage"
|
||||
, "book-category.wiktionary": "Wiktionary"
|
||||
, "book-category.other": "Other"
|
||||
, "text-loading-content": "Loading Content"
|
||||
}
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"AlexanderFF",
|
||||
"Fitoschido",
|
||||
"Laranxa",
|
||||
"Ovruni",
|
||||
"Sinopsistrans",
|
||||
"SpikeShroom",
|
||||
"Vis4valentine"
|
||||
]
|
||||
},
|
||||
"name": "español",
|
||||
"name": "Español",
|
||||
"suggest-full-text-search": "que contenga \"{{{SEARCH_TERMS}}}\"...",
|
||||
"no-such-book": "No existe el libro: {{BOOK_NAME}}",
|
||||
"too-many-books": "Demasiadas solicitudes de libros ({{NB_BOOKS}}) donde el límite es {{LIMIT}}",
|
||||
"no-book-found": "Ningún libro coincide con los criterios de selección.",
|
||||
"no-book-found": "Ningún libro coincide con los criterios de selección",
|
||||
"url-not-found": "La URL solicitada \"{{url}}\" no se encontró en este servidor.",
|
||||
"suggest-search": "Haga una búsqueda de texto completo para <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
|
||||
"random-article-failure": "¡Ups! Error al elegir un artículo aleatorio :(",
|
||||
"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}}",
|
||||
@@ -23,14 +27,36 @@
|
||||
"400-page-heading": "Solicitud inválida",
|
||||
"404-page-title": "Contenido no encontrado",
|
||||
"404-page-heading": "No encontrado",
|
||||
"new-404-page-title": "Página no encontrada",
|
||||
"new-404-page-heading": "Ups. Página no encontrado.",
|
||||
"404-img-text": "¡No encontrado!",
|
||||
"path-was-not-found": "La pagina solicitada no se ha encontrado:",
|
||||
"404-advice.p1": "El contenido que está buscando podría estar disponible pero podría estar localizado en un lugar diferente dentro del archivo ZIM.",
|
||||
"404-advice.p2": "Por favor:",
|
||||
"404-advice.p3": "Intente usar la función de búsqueda para encontrar el contenido que desea",
|
||||
"404-advice.p4": "Mire por palabras clave o títulos relacionados a la información que está buscando",
|
||||
"404-advice.p5": "Este método debería ayudarle a localizar el contenido deseado incluso si el enlace original no funciona correctamente.",
|
||||
"500-page-title": "Error interno del servidor",
|
||||
"500-page-heading": "Error interno del servidor",
|
||||
"500-page-heading": "Ups. La página no funciona.",
|
||||
"500-page-text": "La pagina solicitada no se puede cargar correctamente:",
|
||||
"500-img-text": "La página no está funcionando",
|
||||
"external-link-detected": "Enlace externo detectado",
|
||||
"caution-warning": "¡Precaución!",
|
||||
"external-link-intro": "Usted está apunto de abandonar el lector ZIM de Kiwix para ir a",
|
||||
"external-link-advice.p1": "El enlace al que está tratando de acceder no es parte de su descarga sin conexión y requiere de conexión a internet.",
|
||||
"external-link-advice.p2": "Si usted puede conectarse en linea puede intentar abrir el enlace.",
|
||||
"external-link-advice.p3": "De lo contrario, puede volver al contenido sin conexión de su ZIM utilizando el botón atrás del navegador.",
|
||||
"fulltext-search-unavailable": "Búsqueda de texto completo no disponible",
|
||||
"no-search-results": "El motor de búsqueda de texto completo no está disponible para este contenido.",
|
||||
"search-results-page-title": "Buscar: {{SEARCH_PATTERN}}",
|
||||
"search-results-page-header": "Resultados <b>{{START}}-{{END}}</b> de <b>{{COUNT}}</b> para <b>\"{{{SEARCH_PATTERN}}}\"</b>",
|
||||
"empty-search-results-page-header": "No se encontraron resultados para <b>\"{{{SEARCH_PATTERN}}}\"</b>",
|
||||
"search-result-book-info": "a partir de {{BOOK_TITLE}}",
|
||||
"word-count": "{{COUNT}} palabras",
|
||||
"library-button-text": "Ir a la página de bienvenida",
|
||||
"home-button-text": "Ir a la página principal de '{{BOOK_TITLE}}'",
|
||||
"home-button-text": "Ir a la página principal de '{{{BOOK_TITLE}}}'",
|
||||
"random-page-button-text": "Ir a una página seleccionada al azar",
|
||||
"searchbox-tooltip": "Buscar '{{BOOK_TITLE}}'",
|
||||
"searchbox-tooltip": "Buscar '{{{BOOK_TITLE}}}'",
|
||||
"confusion-of-tongues": "Dos o más libros en diferentes idiomas participarían en la búsqueda, lo que puede llevar a resultados confusos.",
|
||||
"welcome-page-overzealous-filter": "Sin resultados. ¿Quieres <a href=\"{{URL}}\">restablecer el filtro</a> ?",
|
||||
"powered-by-kiwix-html": "Desarrollado por <a href=\"https://kiwix.org\">Kiwix</a>",
|
||||
@@ -40,18 +66,39 @@
|
||||
"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",
|
||||
"torrent-download-link-text": "Archivo de torrent",
|
||||
"torrent-download-alt-text": "descargar torrent",
|
||||
"filter-by-tag": "Filtrar por etiqueta \"{{TAG}}\"",
|
||||
"stop-filtering-by-tag": "Dejar de filtrar por etiqueta \"{{TAG}}\"",
|
||||
"magnet-alt-text": "Descargar mediante enlace Magnet",
|
||||
"torrent-download-link-text": "BitTorrent",
|
||||
"torrent-download-alt-text": "Descargar a través de BitTorrent",
|
||||
"library-opds-feed-all-entries": "Biblioteca OPDS Feed - Todas las entradas",
|
||||
"filter-by-tag": "Filtrar por etiqueta \"{{{TAG}}}\"",
|
||||
"stop-filtering-by-tag": "Dejar de filtrar por etiqueta \"{{{TAG}}}\"",
|
||||
"library-opds-feed-parameterised": "Feed OPDS de la biblioteca: entradas que coinciden con {{#LANG}}\nLanguage: {{LANG}} {{/LANG}}{{#CATEGORY}}\nCategory: {{CATEGORY}} {{/CATEGORY}} {{#TAG}}\nEtiqueta: {{TAG}} {{/TAG}}{{#Q}}\nConsulta: {{Q}} {{/Q}}",
|
||||
"welcome-to-kiwix-server": "Bienvenido al servidor Kiwix",
|
||||
"download-links-heading": "Enlaces de descarga para <b><i>{{BOOK_TITLE}}</i></b>",
|
||||
"download-links-title": "Descargar libro",
|
||||
"preview-book": "Previsualizar"
|
||||
"preview-book": "Previsualizar",
|
||||
"unknown-error": "Error desconocido",
|
||||
"book-category.gutenberg": "Gutenberg",
|
||||
"book-category.iFixit": "iFixit",
|
||||
"book-category.mooc": "MOOC",
|
||||
"book-category.phet": "Phet",
|
||||
"book-category.stack_exchange": "Stack Exchange",
|
||||
"book-category.ted": "Ted",
|
||||
"book-category.vikidia": "Vikidia",
|
||||
"book-category.wikibooks": "Wikilibros",
|
||||
"book-category.wikihow": "wikiHow",
|
||||
"book-category.wikinews": "Wikinoticias",
|
||||
"book-category.wikipedia": "Wikipedia",
|
||||
"book-category.wikiquote": "Wikiquote",
|
||||
"book-category.wikisource": "Wikisource",
|
||||
"book-category.wikispecies": "Wikiespecies",
|
||||
"book-category.wikiversity": "Wikiversidad",
|
||||
"book-category.wikivoyage": "Wikiviajes",
|
||||
"book-category.wiktionary": "Wikcionario",
|
||||
"book-category.other": "Otros",
|
||||
"text-loading-content": "Cargando contenido"
|
||||
}
|
||||
|
||||
@@ -3,28 +3,56 @@
|
||||
"authors": [
|
||||
"MITO",
|
||||
"Nike",
|
||||
"Pyscowicz"
|
||||
"Pyscowicz",
|
||||
"Samoasambia"
|
||||
]
|
||||
},
|
||||
"name": "suomi",
|
||||
"suggest-full-text-search": "sisältää '{{{SEARCH_TERMS}}}'...",
|
||||
"no-such-book": "Kirjaa {{BOOK_NAME}} ei ole olemassa",
|
||||
"url-not-found": "Pyydettyä URL-osoitetta \"{{url}}\" ei löytynyt tältä palvelimelta.",
|
||||
"400-page-title": "Virheellinen pyyntö",
|
||||
"400-page-heading": "Virheellinen pyyntö",
|
||||
"404-page-title": "Sisältöä ei löytynyt",
|
||||
"404-page-heading": "Ei löytynyt",
|
||||
"new-404-page-title": "Sivua ei löytynyt",
|
||||
"new-404-page-heading": "Hupsista. Artikkelia ei löytynyt.",
|
||||
"404-img-text": "Ei löytynyt!",
|
||||
"path-was-not-found": "Pyydettyä polkua ei löytynyt:",
|
||||
"500-page-title": "Sisäinen palvelinvirhe",
|
||||
"500-page-heading": "Sisäinen palvelinvirhe",
|
||||
"500-page-heading": "Hupsista. Sivu ei toimi.",
|
||||
"500-img-text": "Sivu ei toimi",
|
||||
"external-link-detected": "Ulkoinen linkki havaittu",
|
||||
"caution-warning": "Huomio!",
|
||||
"external-link-intro": "Olet poistumassa Kiwixin ZIM-lukijasta siirtyäksesi verkkoon",
|
||||
"search-results-page-title": "Haku: {{SEARCH_PATTERN}}",
|
||||
"word-count": "{{COUNT}} sanaa",
|
||||
"library-button-text": "Siirry tervetulosivulle",
|
||||
"home-button-text": "Siirry kirjan '{{BOOK_TITLE}}' etusivulle",
|
||||
"home-button-text": "Siirry kirjan '{{{BOOK_TITLE}}}' etusivulle",
|
||||
"random-page-button-text": "Siirry satunnaiselle sivulle",
|
||||
"searchbox-tooltip": "Hae '{{BOOK_TITLE}}'",
|
||||
"searchbox-tooltip": "Hae kirjasta '{{{BOOK_TITLE}}}'",
|
||||
"search": "Hae",
|
||||
"book-filtering-all-categories": "Kaikki luokat",
|
||||
"book-filtering-all-languages": "Kaikki kielet",
|
||||
"count-of-matching-books": "{{COUNT}} kirja(a)",
|
||||
"download": "Lataa",
|
||||
"torrent-download-link-text": "Torrent-tiedosto",
|
||||
"magnet-link-text": "Magnet-linkki",
|
||||
"magnet-alt-text": "lataa magnet",
|
||||
"torrent-download-link-text": "BitTorrent",
|
||||
"torrent-download-alt-text": "lataa torrent-tiedosto",
|
||||
"filter-by-tag": "Suodata tunnisteen ”{{TAG}}” mukaan",
|
||||
"download-links-title": "Lataa kirja",
|
||||
"preview-book": "Esikatsele"
|
||||
"preview-book": "Esikatsele",
|
||||
"unknown-error": "Tuntematon virhe",
|
||||
"book-category.wikibooks": "Wikikirjasto",
|
||||
"book-category.wikinews": "Wikiuutiset",
|
||||
"book-category.wikipedia": "Wikipedia",
|
||||
"book-category.wikiquote": "Wikisitaatit",
|
||||
"book-category.wikisource": "Wikiaineisto",
|
||||
"book-category.wikispecies": "Wikispecies",
|
||||
"book-category.wikiversity": "Wikiopisto",
|
||||
"book-category.wikivoyage": "Wikimatkat",
|
||||
"book-category.wiktionary": "Wikisanakirja",
|
||||
"book-category.other": "Muu",
|
||||
"text-loading-content": "Ladataan sisältöä"
|
||||
}
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Adriendelucca",
|
||||
"Benoit74",
|
||||
"Derugon",
|
||||
"Gomoko",
|
||||
"Goombiis",
|
||||
"Melimeli",
|
||||
"Stephane",
|
||||
"Thibaut120094",
|
||||
"Verdy p"
|
||||
"Urhixidur",
|
||||
"Verdy p",
|
||||
"Vikoula5",
|
||||
"Wladek92"
|
||||
]
|
||||
},
|
||||
"name": "Français",
|
||||
@@ -16,6 +24,7 @@
|
||||
"suggest-search": "Faire une recherche en texte intégral de « <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a> »",
|
||||
"random-article-failure": "Oups ! Échec de sélection d’un article aléatoire :(",
|
||||
"invalid-raw-data-type": "{{DATATYPE}} n’est pas une requête valide pour du contenu brut.",
|
||||
"invalid-request": "L'URL demandée \"{{{url}}}\" n'est pas une requête valide.",
|
||||
"no-value-for-arg": "Aucune valeur fournie pour l’argument {{ARGUMENT}}",
|
||||
"no-query": "Aucune requête fournie.",
|
||||
"raw-entry-not-found": "Impossible de trouver l’entrée « {{ENTRY}} » de type « {{DATATYPE}} »",
|
||||
@@ -23,14 +32,36 @@
|
||||
"400-page-heading": "Requête non valide",
|
||||
"404-page-title": "Contenu non trouvé",
|
||||
"404-page-heading": "Non trouvé",
|
||||
"new-404-page-title": "Page non trouvée",
|
||||
"new-404-page-heading": "Oups. Page non trouvée.",
|
||||
"404-img-text": "Non trouvée !",
|
||||
"path-was-not-found": "Le chemin demandé n’a pas été trouvé :",
|
||||
"404-advice.p1": "Le contenu que vous recherchez peut toujours être disponible, mais il peut être positionné à un endroit différent dans le fichier ZIM.",
|
||||
"404-advice.p2": "Veuillez :",
|
||||
"404-advice.p3": "Essayer d’utiliser la fonction de recherche pour trouver le contenu que vous souhaitez",
|
||||
"404-advice.p4": "Rechercher des mots-clés ou des titres liés aux informations que vous recherchez",
|
||||
"404-advice.p5": "Cette approche devrait vous aider à localiser le contenu souhaité, même si le lien d’origine ne fonctionne pas correctement.",
|
||||
"500-page-title": "Erreur interne du serveur",
|
||||
"500-page-heading": "Erreur interne du serveur",
|
||||
"500-page-heading": "Oups, la page ne fonctionne pas.",
|
||||
"500-page-text": "Le chemin demandé ne peut pas être fourni correctement :",
|
||||
"500-img-text": "La page ne fonctionne pas",
|
||||
"external-link-detected": "Lien externe détecté",
|
||||
"caution-warning": "Attention !",
|
||||
"external-link-intro": "Vous êtes sur le point de quitter le lecteur ZIM de Kiwix pour vous connecter à",
|
||||
"external-link-advice.p1": "Le lien que vous essayez de suivre ne fait pas partie de votre forfait hors ligne et nécessite une connexion Internet.",
|
||||
"external-link-advice.p2": "Si vous pouvez vous connecter en ligne, vous pouvez essayer d’ouvrir le lien.",
|
||||
"external-link-advice.p3": "Vous pouvez également revenir au contenu hors ligne de votre ZIM en utilisant le bouton Retour de votre navigateur.",
|
||||
"fulltext-search-unavailable": "Recherche en texte intégral non disponible",
|
||||
"no-search-results": "Le moteur de recherche en texte intégral n’est pas disponible pour ce contenu.",
|
||||
"search-results-page-title": "Rechercher : {{SEARCH_PATTERN}}",
|
||||
"search-results-page-header": "Résultats <b>{{START}}-{{END}}</b> sur<b> {{COUNT}}</b> pour <b>\"{{{SEARCH_PATTERN}}}\"</b>",
|
||||
"empty-search-results-page-header": "Aucun résultat n’a été trouvé pour <b>« {{{SEARCH_PATTERN}}} »</b>",
|
||||
"search-result-book-info": "à partir de {{BOOK_TITLE}}",
|
||||
"word-count": "{{COUNT}} mots",
|
||||
"library-button-text": "Aller à la page de bienvenue",
|
||||
"home-button-text": "Aller à la page principale de « {{BOOK_TITLE}} »",
|
||||
"home-button-text": "Aller à la page principale de « {{{BOOK_TITLE}}} »",
|
||||
"random-page-button-text": "Aller à une page sélectionnée aléatoirement",
|
||||
"searchbox-tooltip": "Rechercher « {{BOOK_TITLE}} »",
|
||||
"searchbox-tooltip": "Rechercher « {{{BOOK_TITLE}}} »",
|
||||
"confusion-of-tongues": "Deux livres ou plus dans des langues différentes participeraient à la recherche, ce qui pourrait conduire à des résultats confus.",
|
||||
"welcome-page-overzealous-filter": "Aucun résultat. Souhaitez-vous <a href=\"{{URL}}\">réinitialiser le filtre</a> ?",
|
||||
"powered-by-kiwix-html": "Propulsé par <a href=\"https://kiwix.org/\">Kiwix</a>",
|
||||
@@ -40,19 +71,31 @@
|
||||
"count-of-matching-books": "{{COUNT}} livre(s)",
|
||||
"download": "Télécharger",
|
||||
"direct-download-link-text": "Direct",
|
||||
"direct-download-alt-text": "téléchargement direct",
|
||||
"hash-download-link-text": "Hachage sha256",
|
||||
"hash-download-alt-text": "télécharger le hachage",
|
||||
"direct-download-alt-text": "Télécharger directement via HTTP(S)",
|
||||
"hash-download-link-text": "Hachage SHA-256",
|
||||
"hash-download-alt-text": "Affiche le hachage SHA-256 du fichier",
|
||||
"magnet-link-text": "Lien Magnet",
|
||||
"magnet-alt-text": "télécharger le lien Magnet",
|
||||
"torrent-download-link-text": "Fichier torrent",
|
||||
"torrent-download-alt-text": "télécharger le torrent",
|
||||
"magnet-alt-text": "Télécharger via le lien Magnet",
|
||||
"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"
|
||||
"preview-book": "Aperçu",
|
||||
"unknown-error": "Erreur inconnue",
|
||||
"book-category.wikibooks": "Wikilivres",
|
||||
"book-category.wikinews": "Wikinews",
|
||||
"book-category.wikipedia": "Wikipédia",
|
||||
"book-category.wikiquote": "Wikiquote",
|
||||
"book-category.wikisource": "Wikisource",
|
||||
"book-category.wikispecies": "Wikispecies",
|
||||
"book-category.wikiversity": "Wikiversité",
|
||||
"book-category.wikivoyage": "Wikivoyage",
|
||||
"book-category.wiktionary": "Wiktionnaire",
|
||||
"book-category.other": "Autre",
|
||||
"text-loading-content": "Chargement du contenu"
|
||||
}
|
||||
|
||||
67
static/skin/i18n/ha.json
Normal file
67
static/skin/i18n/ha.json
Normal file
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Abelidokoo",
|
||||
"El-hussain14",
|
||||
"Rofiatmustapha12",
|
||||
"Smshika",
|
||||
"Yusuf Sa'adu"
|
||||
]
|
||||
},
|
||||
"name": "Turanci",
|
||||
"suggest-full-text-search": "dauke da ''{{{SEARCH_TERMS}}}''...",
|
||||
"no-such-book": "Babu irin wannan littafin: {{BOOK_NAME}}",
|
||||
"too-many-books": "An nemi littattafai da yawa ({{NB_BOOKS}}) inda iyaka shine {{LIMIT}}",
|
||||
"no-book-found": "Babu wani littafi da ya dace da ma'aunin zaɓi",
|
||||
"url-not-found": "Ba a sami URL ɗin da ake nema \"{{url}}\" akan wannan sabar ba.",
|
||||
"suggest-search": "Yi cikakken bincike na rubutu don <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
|
||||
"random-article-failure": "Kash! An kasa ɗaukar labarin bazuwar :(",
|
||||
"invalid-raw-data-type": "{{DATATYPE}} ba ingantaccen buƙatun ɗanyen abun ciki bane.",
|
||||
"invalid-request": "URL ɗin da ake nema \"{{{url}}}\" ba buƙatu mai inganci bane.",
|
||||
"no-value-for-arg": "Babu ƙima da aka bayar don hujja {{ARGUMENT}}",
|
||||
"no-query": "Ba a bayar da tambaya ba.",
|
||||
"raw-entry-not-found": "Ba a iya samun shigarwar {{DATATYPE}} {{ENTRY}}",
|
||||
"400-page-title": "nema mara inganci",
|
||||
"400-page-heading": "nema mara inganci",
|
||||
"404-page-title": "Ba a samo abun ciki ba",
|
||||
"404-page-heading": "Ba a Samu ba",
|
||||
"500-page-title": "Kuskuren na Cikin Saba",
|
||||
"500-page-heading": "Kuskuren na Cikin Saba",
|
||||
"500-page-text": "An sami kuskuren uwar garken ciki. Munyi nadama akan hakan :/",
|
||||
"fulltext-search-unavailable": "Babu binciken cikakken rubutu",
|
||||
"no-search-results": "Babu injin binciken cikakken rubutu don wannan abun ciki.",
|
||||
"search-results-page-title": "Bincika: {{SEARCH_PATTERN}}",
|
||||
"search-results-page-header": "Sakamako <b>{{START}}-{{END}}</b> na <b>{{COUNT}}</b> na <b>\"{{{SEARCH_PATTERN}}}\"</b>",
|
||||
"empty-search-results-page-header": "Ba a sami sakamakon <b>\"{{{SEARCH_PATTERN}}}\"</b>",
|
||||
"search-result-book-info": "daga {{BOOK_TITLE}}",
|
||||
"word-count": "{{COUNT}} kalmomi",
|
||||
"library-button-text": "Je zuwa shafin maraba",
|
||||
"home-button-text": "Jeka babban shafin '{{BOOK_TITLE}}'",
|
||||
"random-page-button-text": "Je zuwa shafin da aka zaɓa ba da gangan ba",
|
||||
"searchbox-tooltip": "Bincika '{{BOOK_TITLE}}'",
|
||||
"confusion-of-tongues": "Littattafai biyu ko fiye a cikin harsuna daban-daban za su shiga cikin bincike, wanda zai iya haifar da sakamako mai ruɗani.",
|
||||
"welcome-page-overzealous-filter": "Babu sakamako. Kuna so a <a href=\"{{URL}}\">sake saita tace</a>?",
|
||||
"powered-by-kiwix-html": "<a href=\"https://kiwix.org\">Kiwix</a> ne ke ƙarfafa shi",
|
||||
"search": "Nema",
|
||||
"book-filtering-all-categories": "Dukkanin nau'o'in",
|
||||
"book-filtering-all-languages": "Duka harsuna",
|
||||
"count-of-matching-books": "{{COUNT}} littafi(s)",
|
||||
"download": "Sauke",
|
||||
"direct-download-link-text": "Kai tsaye",
|
||||
"direct-download-alt-text": "saukewa kai tsaye",
|
||||
"hash-download-link-text": "Sha256 hash",
|
||||
"hash-download-alt-text": "sauke hash",
|
||||
"magnet-link-text": "Magnet link",
|
||||
"magnet-alt-text": "Magnet ɗin saukewa",
|
||||
"torrent-download-link-text": "Torrent fayil",
|
||||
"torrent-download-alt-text": "download torrent",
|
||||
"library-opds-feed-all-entries": "Ciyarwar OPDS Library - Duk shigarwar",
|
||||
"filter-by-tag": "Tace da alamar \"{{TAG}}\"",
|
||||
"stop-filtering-by-tag": "Dakatar da tacewa ta hanyar \"{{TAG}}\"",
|
||||
"library-opds-feed-parameterised": " OPDS ciyar wa OPDS- entries matching {{#LANG}}\nLanguage: {{LANG}} {{/LANG}}{{#CATEGORY}}\nCategory: {{CATEGORY}} {{/CATEGORY}}{{#TAG}}\nTag: {{TAG}} {{/TAG}}{{#Q}}\nQuery: {{Q}} {{/Q}}",
|
||||
"welcome-to-kiwix-server": "Barka da zowa manhajar Kiwix",
|
||||
"download-links-heading": "Bring out ways through which people can join hands together .{{BOOK_TITLE}}",
|
||||
"download-links-title": "Sauke littafin",
|
||||
"preview-book": "Dubawa",
|
||||
"unknown-error": "Kuskuren da ba a sani ba"
|
||||
}
|
||||
@@ -14,6 +14,7 @@
|
||||
"suggest-search": "לעשות חיפוש טקסט מלא עבור <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
|
||||
"random-article-failure": "אוי! לא עבדה בחירת ערך אקראי :(",
|
||||
"invalid-raw-data-type": "{{DATATYPE}} הוא לא בקשה תקינה של תוכן גולמי.",
|
||||
"invalid-request": "הכתובת המבוקשת \"{{{url}}}\" אינה בקשה תקינה.",
|
||||
"no-value-for-arg": "לא סופק ערך לארגומנט {{ARGUMENT}}",
|
||||
"no-query": "לא סופקה שאילתה.",
|
||||
"raw-entry-not-found": "לא ניתן למצוא את רשומת ה־{{DATATYPE}} בשם {{ENTRY}}",
|
||||
@@ -21,14 +22,30 @@
|
||||
"400-page-heading": "בקשה בלתי־תקינה",
|
||||
"404-page-title": "התוכן לא נמצא",
|
||||
"404-page-heading": "לא נמצא",
|
||||
"new-404-page-title": "הדף לא נמצא",
|
||||
"new-404-page-heading": "אופס. הדף לא נמצא.",
|
||||
"404-img-text": "לא נמצא!",
|
||||
"path-was-not-found": "הנתיב המבוקש לא נמצא:",
|
||||
"404-advice.p2": "בבקשה:",
|
||||
"404-advice.p3": "לנסות להשתמש ביכולת החיפוש כדי למצוא את התוכן המבוקש",
|
||||
"500-page-title": "שגיאת שרת פנימית",
|
||||
"500-page-heading": "שגיאת שרת פנימית",
|
||||
"500-page-heading": "אופס. הדף לא עובד.",
|
||||
"500-page-text": "אי אפשר למסור את הנתיב המבוקש כראוי:",
|
||||
"500-img-text": "הדף לא עובד",
|
||||
"external-link-detected": "התגלה קישור חיצוני",
|
||||
"caution-warning": "זהירות!",
|
||||
"external-link-advice.p2": "אם יש לך איך להתחבר לאינטרנט, אפשר לנסות לפתוח את הקישור.",
|
||||
"fulltext-search-unavailable": "חיפוש בטקסט מלא אינו זמין",
|
||||
"no-search-results": "מנוע החיפוש בטקסט מלא אינו זמין עבור התוכן הזה.",
|
||||
"search-results-page-title": "חיפוש: {{SEARCH_PATTERN}}",
|
||||
"search-results-page-header": "תוצאות <b>{{START}} עד {{END}}</b> מתוך <b>{{COUNT}}</b> עבור <b>\"{{{SEARCH_PATTERN}}}\"</b>",
|
||||
"empty-search-results-page-header": "לא נמצאו תוצאות עבור <b>\"{{{SEARCH_PATTERN}}}\"</b>",
|
||||
"search-result-book-info": "מתוך {{BOOK_TITLE}}",
|
||||
"word-count": "{{COUNT}} מילים",
|
||||
"library-button-text": "מעבר לדף הבית \"ברוך בואך\"",
|
||||
"home-button-text": "מעבר לדף הראשי של \"{{BOOK_TITLE}}\"",
|
||||
"home-button-text": "מעבר לדף הראשי של ‚{{{BOOK_TITLE}}}’",
|
||||
"random-page-button-text": "מעבר לדף שנבחר אקראית",
|
||||
"searchbox-tooltip": "חיפוש \"{{BOOK_TITLE}}\"",
|
||||
"searchbox-tooltip": "חיפוש אחר ‚{{{BOOK_TITLE}}}’",
|
||||
"confusion-of-tongues": "שני ספרים או יותר בשפות שונות ישתתפו בחיפוש, מה שעלול להוביל לתוצאות מבלבלות.",
|
||||
"welcome-page-overzealous-filter": "אין תוצאות. האם <a href=\"{{URL}}\">לאפס את המסנן</a>?",
|
||||
"powered-by-kiwix-html": "מופעל על־ידי <a href=\"https://kiwix.org\">Kiwix</a>",
|
||||
@@ -38,19 +55,33 @@
|
||||
"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": "הורדה ישירה דרך HTTP(S)",
|
||||
"hash-download-link-text": "סיכום ביקורת Sha256",
|
||||
"hash-download-alt-text": "הצגת סיכום ביקורת SHA-256",
|
||||
"magnet-link-text": "קישור Magnet",
|
||||
"magnet-alt-text": "הורדת magnet",
|
||||
"torrent-download-link-text": "קובץ טורנט",
|
||||
"torrent-download-alt-text": "הורדת טורנט",
|
||||
"magnet-alt-text": "הורדה באמצעות קישור magnet",
|
||||
"torrent-download-link-text": "ביטורנט",
|
||||
"torrent-download-alt-text": "הורדה באמצעות ביטורנט",
|
||||
"library-opds-feed-all-entries": "הזנת ספריית OPDS - כל הרשומות",
|
||||
"filter-by-tag": "סינון לפי התג \"{{TAG}}\"",
|
||||
"stop-filtering-by-tag": "להפסיק סינון לפי התג \"{{TAG}}\"",
|
||||
"filter-by-tag": "לסנן לפי התג \"{{{TAG}}}\"",
|
||||
"stop-filtering-by-tag": "להפסיק סינון לפי התג \"{{{TAG}}}\"",
|
||||
"library-opds-feed-parameterised": "הזנת ספריית OPDS - רשומות שתואמות ל{{#LANG}}\nשפה: {{LANG}} {{/LANG}}{{#CATEGORY}}\nקטגוריה: {{CATEGORY}} {{/CATEGORY}}{{#TAG}}\nתג: {{TAG}} {{/TAG}}{{#Q}}\nשאילתה: {{Q}} {{/Q}}",
|
||||
"welcome-to-kiwix-server": "ברוך בואך לשרת קיוויקס",
|
||||
"download-links-heading": "הורדת קישורים עבור <b><i>{{BOOK_TITLE}}</i></b>",
|
||||
"download-links-title": "הורדת ספר",
|
||||
"preview-book": "תצוגה מקדימה"
|
||||
"preview-book": "תצוגה מקדימה",
|
||||
"unknown-error": "שגיאה בלתי־ידועה",
|
||||
"book-category.gutenberg": "גוטנברג",
|
||||
"book-category.vikidia": "ויקידיה",
|
||||
"book-category.wikibooks": "ויקיספר",
|
||||
"book-category.wikinews": "ויקיחדשות",
|
||||
"book-category.wikipedia": "ויקיפדיה",
|
||||
"book-category.wikiquote": "ויקיציטוט",
|
||||
"book-category.wikisource": "ויקיטקסט (Wikisource)",
|
||||
"book-category.wikispecies": "ויקימינים",
|
||||
"book-category.wikiversity": "ויקיברסיטה",
|
||||
"book-category.wikivoyage": "ויקימסע",
|
||||
"book-category.wiktionary": "ויקימילון",
|
||||
"book-category.other": "אחר",
|
||||
"text-loading-content": "התוכן נטען"
|
||||
}
|
||||
|
||||
@@ -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": "अन्य"
|
||||
}
|
||||
|
||||
39
static/skin/i18n/hu.json
Normal file
39
static/skin/i18n/hu.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Eukarióta",
|
||||
"Urbalazs"
|
||||
]
|
||||
},
|
||||
"name": "Magyar",
|
||||
"400-page-title": "Érvénytelen kérés",
|
||||
"400-page-heading": "Érvénytelen kérés",
|
||||
"404-page-title": "A tartalom nem található",
|
||||
"404-page-heading": "Nem található",
|
||||
"new-404-page-title": "Az oldal nem található",
|
||||
"new-404-page-heading": "Hoppá! Az oldal nem található.",
|
||||
"404-img-text": "Nem található!",
|
||||
"path-was-not-found": "A kért útvonal nem található:",
|
||||
"404-advice.p1": "A keresett tartalom továbbra is elérhető lehet, de előfordulhat, hogy más helyen található a ZIM-fájlon belül.",
|
||||
"search": "Keresés",
|
||||
"download": "Letöltés",
|
||||
"hash-download-link-text": "SHA-256 ellenőrzőösszeg",
|
||||
"hash-download-alt-text": "SHA-256 fájl-ellenőrzőösszeg megjelenítése",
|
||||
"torrent-download-link-text": "BitTorrent",
|
||||
"welcome-to-kiwix-server": "Üdvözli a Kiwix-kiszolgáló!",
|
||||
"download-links-heading": "Letöltési hivatkozások ehhez: <b><i>{{BOOK_TITLE}}</i></b>",
|
||||
"download-links-title": "Könyv letöltése",
|
||||
"preview-book": "Előnézet",
|
||||
"unknown-error": "Ismeretlen hiba",
|
||||
"book-category.wikibooks": "Wikikönyvek",
|
||||
"book-category.wikinews": "Wikihírek",
|
||||
"book-category.wikipedia": "Wikipédia",
|
||||
"book-category.wikiquote": "Wikidézet",
|
||||
"book-category.wikisource": "Wikiforrás",
|
||||
"book-category.wikispecies": "Wikifajok",
|
||||
"book-category.wikiversity": "Wikiegyetem",
|
||||
"book-category.wikivoyage": "Wikivoyage",
|
||||
"book-category.wiktionary": "Wikiszótár",
|
||||
"book-category.other": "Egyéb",
|
||||
"text-loading-content": "Tartalom betöltése"
|
||||
}
|
||||
@@ -13,9 +13,20 @@
|
||||
"400-page-heading": "Անվավեր հարցում",
|
||||
"404-page-title": "Սխալ հասցե",
|
||||
"404-page-heading": "Սխալ հասցե",
|
||||
"500-img-text": "Էջը չի աշխատում",
|
||||
"library-button-text": "Գրադարանի էջ",
|
||||
"home-button-text": "Դեպի '{{BOOK_TITLE}}'֊ի գլխավոր էջը",
|
||||
"random-page-button-text": "Բացել պատահական էջ",
|
||||
"searchbox-tooltip": "Որոնել '{{BOOK_TITLE}}'֊ում",
|
||||
"book-filtering-all-categories": "Բոլոր կատեգորիաներ"
|
||||
"book-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": "Այլ"
|
||||
}
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
"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.",
|
||||
"raw-entry-not-found": "Non pote trovar le entrata {{ENTRY}} del typo {{DATATYPE}}",
|
||||
@@ -21,14 +22,36 @@
|
||||
"400-page-heading": "Requesta invalide",
|
||||
"404-page-title": "Contento non trovate",
|
||||
"404-page-heading": "Non trovate",
|
||||
"new-404-page-title": "Pagina non trovate",
|
||||
"new-404-page-heading": "Pagina non trovate.",
|
||||
"404-img-text": "Non trovate!",
|
||||
"path-was-not-found": "Le percurso requestate non ha essite trovate:",
|
||||
"404-advice.p1": "Le contento que tu cerca pote esser ancora disponibile, ma illo pote esser situate alterubi in le file ZIM.",
|
||||
"404-advice.p2": "Per favor:",
|
||||
"404-advice.p3": "Tenta usar le function de recerca pro trovar le contento desirate",
|
||||
"404-advice.p4": "Cerca parolas-clave o titulos associate al information que tu cerca",
|
||||
"404-advice.p5": "Iste approche deberea adjutar te a localisar le contento desirate, mesmo si le ligamine original non functiona correctemente.",
|
||||
"500-page-title": "Error interne del servitor",
|
||||
"500-page-heading": "Error interne del servitor",
|
||||
"500-page-heading": "Le pagina non functiona.",
|
||||
"500-page-text": "Le percurso requestate non pote esser livrate correctemente:",
|
||||
"500-img-text": "Le pagina non functiona",
|
||||
"external-link-detected": "Ligamine externe detegite",
|
||||
"caution-warning": "Attention!",
|
||||
"external-link-intro": "Tu es sur le puncto de quitar le lector ZIM de Kiwix pro connecter te al rete e visitar",
|
||||
"external-link-advice.p1": "Le ligamine al qual tu tenta acceder non face parte de nostre pacchetto de uso foras de linea e require un connexion a internet.",
|
||||
"external-link-advice.p2": "Si tu pote connecter te al rete, tu pote tentar aperir le ligamine.",
|
||||
"external-link-advice.p3": "Si non, tu pote retornar al contento de uso foras de linea de tu ZIM usante le button Retro del navigator.",
|
||||
"fulltext-search-unavailable": "Le recerca in texto complete es indisponibile",
|
||||
"no-search-results": "Le motor de recerca in texto complete non es disponibile pro iste contento.",
|
||||
"search-results-page-title": "Cercar: {{SEARCH_PATTERN}}",
|
||||
"search-results-page-header": "Resultatos <b>{{START}}-{{END}}</b> de <b>{{COUNT}}</b> pro <b>“{{{SEARCH_PATTERN}}}”</b>",
|
||||
"empty-search-results-page-header": "Necun resultato ha essite trovate pro <b>“{{{SEARCH_PATTERN}}}”</b>",
|
||||
"search-result-book-info": "de {{BOOK_TITLE}}",
|
||||
"word-count": "{{COUNT}} parolas",
|
||||
"library-button-text": "Ir al pagina de benvenita",
|
||||
"home-button-text": "Ir al pagina principal de ''{{BOOK_TITLE}}",
|
||||
"home-button-text": "Ir al pagina principal de ‘{{{BOOK_TITLE}}}’",
|
||||
"random-page-button-text": "Ir a un pagina seligite aleatorimente",
|
||||
"searchbox-tooltip": "Cercar '{{BOOK_TITLE}}'",
|
||||
"searchbox-tooltip": "Cercar in ‘{{{BOOK_TITLE}}}’",
|
||||
"confusion-of-tongues": "Duo o plus libros in differente linguas participarea in le recerca, lo que pote menar a resultatos confuse.",
|
||||
"welcome-page-overzealous-filter": "Nulle resultato. Vole tu <a href=\"{{URL}}\">reinitialisar le filtro</a>?",
|
||||
"powered-by-kiwix-html": "Actionate per <a href=\"https://kiwix.org\">Kiwix</a>",
|
||||
@@ -38,19 +61,31 @@
|
||||
"count-of-matching-books": "{{COUNT}} libro(s)",
|
||||
"download": "Discargar",
|
||||
"direct-download-link-text": "Directe",
|
||||
"direct-download-alt-text": "discargamento directe",
|
||||
"hash-download-link-text": "Hash SHA256",
|
||||
"hash-download-alt-text": "hash del discargamento",
|
||||
"direct-download-alt-text": "Discargamento directe per HTTP(S)",
|
||||
"hash-download-link-text": "Summa de controlo SHA-256",
|
||||
"hash-download-alt-text": "Monstrar le summa de controlo SHA-256 del file",
|
||||
"magnet-link-text": "Ligamine Magnet",
|
||||
"magnet-alt-text": "ligamine \"magnet\" de discargamento",
|
||||
"torrent-download-link-text": "File Torrent",
|
||||
"torrent-download-alt-text": "discargar Torrent",
|
||||
"magnet-alt-text": "Discargar con ligamine Magnet",
|
||||
"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"
|
||||
"preview-book": "Previsualisation",
|
||||
"unknown-error": "Error incognite",
|
||||
"book-category.wikibooks": "Wikilibros",
|
||||
"book-category.wikinews": "Wikinovas",
|
||||
"book-category.wikipedia": "Wikipedia",
|
||||
"book-category.wikiquote": "Wikiquote",
|
||||
"book-category.wikisource": "Wikisource",
|
||||
"book-category.wikispecies": "Wikispecies",
|
||||
"book-category.wikiversity": "Wikiversitate",
|
||||
"book-category.wikivoyage": "Wikiviage",
|
||||
"book-category.wiktionary": "Wiktionario",
|
||||
"book-category.other": "Altere",
|
||||
"text-loading-content": "Carga contento"
|
||||
}
|
||||
|
||||
74
static/skin/i18n/id.json
Normal file
74
static/skin/i18n/id.json
Normal 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"
|
||||
}
|
||||
65
static/skin/i18n/ig.json
Normal file
65
static/skin/i18n/ig.json
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Accuratecy051",
|
||||
"Ngostary2k",
|
||||
"Oby Ezeilo"
|
||||
]
|
||||
},
|
||||
"name": "Bekee",
|
||||
"suggest-full-text-search": "nwere {{{SEARCH_TERMS}}}'",
|
||||
"no-such-book": "Enweghị akwụkwọ dị otú a: {{BOOK_NAME}}",
|
||||
"too-many-books": "Arịrịọ ọtụtụ akwụkwọ ({{NB_BOOKS}}) ebe oke bụ {{LIMIT}}",
|
||||
"no-book-found": "Ọ nweghị akwụkwọ dabara na nhọpụta nhọrọ",
|
||||
"url-not-found": "Ahụghị URL a rịọrọ \"{{url}}\" na nkesa a.",
|
||||
"suggest-search": "Mee ọchụchọ ederede zuru oke maka <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
|
||||
"random-article-failure": "Ee! Ịhọrọ akụkọ enweghị usoro :(",
|
||||
"invalid-raw-data-type": "{{DATATYPE}} abụghị arịrịọ ziri ezi maka ọdịnaya raw.",
|
||||
"invalid-request": "Arịrịọ gbasara URL \"{{{url}}}\" e zighi ezi.",
|
||||
"no-value-for-arg": "Ọ nweghị uru enyere maka arụmụka {{ARGUMENT}}",
|
||||
"no-query": "Ọnweghị ajụjụ enyere.",
|
||||
"raw-entry-not-found": "Enweghị ike ịchọta ntinye {{DATATYPE}} {{ENTRY}}",
|
||||
"400-page-title": "Arịrịọ na-ezighi ezi",
|
||||
"400-page-heading": "Arịrịọ na-ezighi ezi",
|
||||
"404-page-title": "Ahụghị ọdịnaya",
|
||||
"404-page-heading": "Ahụghị",
|
||||
"500-page-title": "Mperi Sava Ime",
|
||||
"500-page-heading": "Mperi Sava Ime",
|
||||
"500-page-text": "Enwere mperi ihe nkesa dị n'ime. Ọ dị anyị nwute na nke ahụ :/",
|
||||
"fulltext-search-unavailable": "Ọchịchọ ederede zuru ezu adịghị",
|
||||
"no-search-results": "Igwe nchọta ederede zuru oke adịghị maka ọdịnaya a.",
|
||||
"search-results-page-title": "Chọọ: {{SEARCH_PATTERN}}",
|
||||
"search-results-page-header": "Rịzọlt ga <b>{{START}}-{{END}}</b> nke <b>{{COUNT}}</b> maka <b>\"{{{SEARCH_PATTERN}}}\"</b>",
|
||||
"empty-search-results-page-header": "Ọnweghị rịzọlt ahụrụ maka <b>\"{{{SEARCH_PATTERN}}}\"</b>",
|
||||
"search-result-book-info": "sitere na {{BOOK_TITLE}}",
|
||||
"word-count": "Okwu {{COUNT}}",
|
||||
"library-button-text": "Gaa na ibe nnabata",
|
||||
"home-button-text": "Gaa na isi ibe nke '{{BOOK_TITLE}}'",
|
||||
"random-page-button-text": "Gaa na ibe ahọpụtara enweghị usoro",
|
||||
"searchbox-tooltip": "Chọọ '{{BOOK_TITLE}}'",
|
||||
"confusion-of-tongues": "Akwụkwọ abụọ ma ọ bụ karịa n'asụsụ dị iche iche ga-esonye na nchọ, nke nwere ike ibute nsonaazụ mgbagwoju anya.",
|
||||
"welcome-page-overzealous-filter": "Enweghị nsonaazụ. Ọ ga-amasị gị <a href=\"{{URL}}\">ịtọgharịa nzacha</a> ?",
|
||||
"powered-by-kiwix-html": "<a href=\"https://kiwix.org\">Kiwix</a> kwadoro ya",
|
||||
"search": "Chọọ",
|
||||
"book-filtering-all-categories": "Nkeji niile",
|
||||
"book-filtering-all-languages": "Asụsụ niile",
|
||||
"count-of-matching-books": "akwụkwọ {{COUNT}}",
|
||||
"download": "Budata",
|
||||
"direct-download-link-text": "Gosi",
|
||||
"direct-download-alt-text": "nbudata ozugbo",
|
||||
"hash-download-link-text": "Sha256 hash",
|
||||
"hash-download-alt-text": "budata hash",
|
||||
"magnet-link-text": "Njikọ magnet",
|
||||
"magnet-alt-text": "ibudata magnet",
|
||||
"torrent-download-link-text": " faịlụ nke Torrent",
|
||||
"torrent-download-alt-text": "Budata torrent",
|
||||
"library-opds-feed-all-entries": "Ọbá akwụkwọ OPDS Feed - Ihe niile",
|
||||
"filter-by-tag": "Wepụta site na mkpado \"{{TAG}}\"",
|
||||
"stop-filtering-by-tag": "Kwụsị nzacha site na mkpado \"{{TAG}}\"",
|
||||
"library-opds-feed-parameterised": "Ọbá akwụkwọ OPDS nri - ndenye dabara na {{#LANG}}\nAsụsụ: {{LANG}} {{/LANG}}{{#CATEGORY}}\n Kategori: {{CATEGORY}} {{/CATEGORY}} {{#TAG}}\nTag: {{TAG}} {{/TAG}}{{#Q}}\n Ajụjụ: {{Q}} {{/Q}}",
|
||||
"welcome-to-kiwix-server": "Nabata na Kiwix Server",
|
||||
"download-links-heading": "Budata njikọ maka <b><i>{{BOOK_TITLE}}</i></b>",
|
||||
"download-links-title": "Budata akwụkwọ",
|
||||
"preview-book": "Ziwe nkirimaàtụ̀",
|
||||
"unknown-error": "amaghị m njehie"
|
||||
}
|
||||
@@ -3,7 +3,10 @@
|
||||
"authors": [
|
||||
"Albano",
|
||||
"Beta16",
|
||||
"McDutchie"
|
||||
"Clorofolle",
|
||||
"Luca.favorido",
|
||||
"McDutchie",
|
||||
"Wheelygay"
|
||||
]
|
||||
},
|
||||
"name": "italiano",
|
||||
@@ -14,21 +17,53 @@
|
||||
"url-not-found": "L'URL richiesto \"{{url}}\" non è stato trovato in questo server.",
|
||||
"suggest-search": "Effettua una ricerca di testo completo per <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
|
||||
"random-article-failure": "Ops! Impossibile selezionare un articolo casuale :(",
|
||||
"invalid-request": "L'URL richiesto \"{{{url}}}\" non è una richiesta valida.",
|
||||
"no-value-for-arg": "Nessun valore fornito per l'argomento {{ARGUMENT}}",
|
||||
"400-page-title": "Richiesta non valida",
|
||||
"400-page-heading": "Richiesta non valida",
|
||||
"404-page-title": "Contenuto non trovato",
|
||||
"404-page-heading": "Non trovato",
|
||||
"new-404-page-title": "Pagina non trovata",
|
||||
"new-404-page-heading": "Oops. Pagina non trovata.",
|
||||
"404-img-text": "Non trovato!",
|
||||
"500-page-title": "Errore interno del server",
|
||||
"500-page-heading": "Errore interno del server",
|
||||
"500-page-heading": "Oops. La pagina non funziona.",
|
||||
"500-page-text": "Il percorso richiesto non può essere fornito correttamente:",
|
||||
"caution-warning": "Attenzione!",
|
||||
"search-results-page-title": "Cerca: {{SEARCH_PATTERN}}",
|
||||
"search-results-page-header": "Risultati <b>{{START}}-{{END}}</b> di <b>{{COUNT}}</b> per <b>\"{{{SEARCH_PATTERN}}}\"</b>",
|
||||
"empty-search-results-page-header": "Non è stato trovato alcun risultato per <b>\"{{{SEARCH_PATTERN}}}\"</b>",
|
||||
"search-result-book-info": "da {{BOOK_TITLE}}",
|
||||
"word-count": "{{COUNT}} parole",
|
||||
"library-button-text": "Vai alla pagina di benvenuto",
|
||||
"home-button-text": "Vai alla pagina principale di '{{BOOK_TITLE}}'",
|
||||
"home-button-text": "Vai alla pagina principale di '{{{BOOK_TITLE}}}'",
|
||||
"random-page-button-text": "Vai a una pagina selezionata casualmente",
|
||||
"searchbox-tooltip": "Cerca '{{BOOK_TITLE}}'",
|
||||
"searchbox-tooltip": "Cerca '{{{BOOK_TITLE}}}'",
|
||||
"welcome-page-overzealous-filter": "Nessun risultato. Vuoi <a href=\"{{URL}}\">reimpostare il filtro</a>?",
|
||||
"search": "Cerca",
|
||||
"book-filtering-all-categories": "Tutte le categorie",
|
||||
"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"
|
||||
"preview-book": "Anteprima",
|
||||
"unknown-error": "Errore sconosciuto",
|
||||
"book-category.wikibooks": "Wikibooks",
|
||||
"book-category.wikinews": "Wikinotizie",
|
||||
"book-category.wikipedia": "Wikipedia",
|
||||
"book-category.wikiquote": "Wikiquote",
|
||||
"book-category.wikisource": "Wikisource",
|
||||
"book-category.wikispecies": "Wikispecies",
|
||||
"book-category.wikiversity": "Wikiversità",
|
||||
"book-category.wikivoyage": "Wikivoyage",
|
||||
"book-category.wiktionary": "Wikizionario",
|
||||
"book-category.other": "Altro",
|
||||
"text-loading-content": "Caricamento contenuto"
|
||||
}
|
||||
|
||||
@@ -1,19 +1,80 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"YeBoy371",
|
||||
"Ykhwong"
|
||||
]
|
||||
},
|
||||
"name": "한국어",
|
||||
"suggest-full-text-search": "'{{{SEARCH_TERMS}}}' 포함...",
|
||||
"no-such-book": "해당 책이 없습니다: {{BOOK_NAME}}",
|
||||
"too-many-books": "요청된 책이 너무 많습니다. ({{NB_BOOKS}}) 한도는 {{LIMIT}}입니다.",
|
||||
"no-book-found": "선택 기준에 해당하는 책이 없습니다",
|
||||
"url-not-found": "\"{{url}}\" 요청 URL은 이 서버에서 찾을 수 없습니다.",
|
||||
"suggest-search": "<a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>에 대한 전문 검색을 수행해 보세요",
|
||||
"random-article-failure": "이런! 임의 문서를 선택하지 못했습니다 :(",
|
||||
"invalid-raw-data-type": "{{DATATYPE}} 값은 원시 콘텐츠에 대한 유효한 요청이 아닙니다.",
|
||||
"invalid-request": "\"{{{url}}}\" 요청 URL은 유효한 요청이 아닙니다.",
|
||||
"no-value-for-arg": "{{ARGUMENT}} 인수에 지정된 값이 없습니다",
|
||||
"no-query": "지정된 쿼리가 없습니다.",
|
||||
"raw-entry-not-found": "{{DATATYPE}} 항목인 {{ENTRY}} 항목을 찾을 수 없습니다",
|
||||
"400-page-title": "잘못된 요청",
|
||||
"400-page-heading": "잘못된 요청",
|
||||
"404-page-title": "내용이 없습니다",
|
||||
"404-page-heading": "찾을 수 없음",
|
||||
"new-404-page-title": "페이지를 찾을 수 없습니다",
|
||||
"new-404-page-heading": "이런. 페이지를 찾을 수 없습니다.",
|
||||
"404-img-text": "찾을 수 없습니다!",
|
||||
"path-was-not-found": "요청한 경로를 찾을 수 없습니다:",
|
||||
"404-advice.p3": "원하는 콘텐츠를 찾으려면 검색 기능을 사용해 보세요",
|
||||
"500-page-title": "내부 서버 오류",
|
||||
"500-page-heading": "내부 서버 오류",
|
||||
"500-page-heading": "죄송합니다. 문서가 동작하지 않습니다.",
|
||||
"500-page-text": "요청된 경로를 제대로 전달할 수 없습니다:",
|
||||
"500-img-text": "문서가 동작하지 않습니다",
|
||||
"external-link-detected": "외부 링크가 발견되었습니다",
|
||||
"caution-warning": "경고!",
|
||||
"fulltext-search-unavailable": "전문 검색을 사용할 수 없습니다",
|
||||
"no-search-results": "이 콘텐츠에는 전문 검색 엔진을 사용할 수 없습니다.",
|
||||
"search-results-page-title": "검색: {{SEARCH_PATTERN}}",
|
||||
"search-results-page-header": "<b>\"{{{SEARCH_PATTERN}}}\"</b>에 대한 <b>{{COUNT}}</b>개 중 <b>{{START}}-{{END}}</b> 결과",
|
||||
"empty-search-results-page-header": "<b>\"{{{SEARCH_PATTERN}}}\"</b>의 결과가 없습니다",
|
||||
"search-result-book-info": "{{BOOK_TITLE}}에서",
|
||||
"word-count": "단어 {{COUNT}}개",
|
||||
"home-button-text": "'{{{BOOK_TITLE}}}'의 메인 페이지로 이동",
|
||||
"random-page-button-text": "무작위로 선택된 문서로 이동",
|
||||
"preview-book": "미리 보기"
|
||||
"searchbox-tooltip": "'{{{BOOK_TITLE}}}' 검색",
|
||||
"welcome-page-overzealous-filter": "결과가 없습니다. <a href=\"{{URL}}\">필터를 재설정</a>하시겠습니까?",
|
||||
"powered-by-kiwix-html": "<a href=\"https://kiwix.org\">Kiwix</a>에서 제공",
|
||||
"search": "검색",
|
||||
"book-filtering-all-categories": "모든 분류",
|
||||
"book-filtering-all-languages": "모든 언어",
|
||||
"count-of-matching-books": "책 {{COUNT}}권",
|
||||
"download": "다운로드",
|
||||
"direct-download-link-text": "직접",
|
||||
"direct-download-alt-text": "HTTP(S)를 통해 직접 다운로드",
|
||||
"hash-download-link-text": "SHA-256 체크섬",
|
||||
"hash-download-alt-text": "SHA-256 파일 체크섬 표시",
|
||||
"magnet-link-text": "마그넷 링크",
|
||||
"magnet-alt-text": "마그넷 링크를 통해 다운로드",
|
||||
"torrent-download-link-text": "비트토렌트",
|
||||
"torrent-download-alt-text": "비트토렌트를 통해 다운로드",
|
||||
"library-opds-feed-all-entries": "라이브러리 OPDS 피드 - 모든 항목",
|
||||
"filter-by-tag": "\"{{{TAG}}}\" 태그로 필터링",
|
||||
"stop-filtering-by-tag": "\"{{{TAG}}}\" 태그로 필터링 중지",
|
||||
"library-opds-feed-parameterised": "라이브러리 OPDS 피드 - {{#LANG}} 일치 항목\n언어: {{LANG}} {{/LANG}}{{#CATEGORY}}\n분류: {{CATEGORY}} {{/CATEGORY}}{{#TAG}}\n태그: {{TAG}} {{/TAG}}{{#Q}}\n쿼리: {{Q}} {{/Q}}",
|
||||
"welcome-to-kiwix-server": "Kiwix Server에 오신 것을 환영합니다",
|
||||
"download-links-heading": "<b><i>{{BOOK_TITLE}}</i></b>의 링크 다운로드",
|
||||
"download-links-title": "책 다운로드",
|
||||
"preview-book": "미리 보기",
|
||||
"unknown-error": "알 수 없는 오류",
|
||||
"book-category.wikibooks": "위키책",
|
||||
"book-category.wikinews": "위키뉴스",
|
||||
"book-category.wikipedia": "위키백과",
|
||||
"book-category.wikiquote": "위키인용집",
|
||||
"book-category.wikisource": "위키문헌",
|
||||
"book-category.wikispecies": "위키생물종",
|
||||
"book-category.wikiversity": "위키배움터",
|
||||
"book-category.wikivoyage": "위키여행",
|
||||
"book-category.wiktionary": "위키낱말사전",
|
||||
"book-category.other": "기타"
|
||||
}
|
||||
|
||||
@@ -7,16 +7,32 @@
|
||||
]
|
||||
},
|
||||
"name": "Lëtzebuergesch",
|
||||
"suggest-full-text-search": "enthält '{{{SEARCH_TERMS}}}'...",
|
||||
"no-such-book": "Buch net fonnt: {{BOOK_NAME}}",
|
||||
"too-many-books": "Ze vill Bicher ugefrot ({{NB_BOOKS}}), d'Limitt läit bei {{LIMIT}}",
|
||||
"url-not-found": "Déi ugefroten URL „{{url}}“ gouf op dësem Server net fonnt.",
|
||||
"suggest-search": "Maacht eng Volltext-Sich fir <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
|
||||
"random-article-failure": "Ups! Et konnt keen zoufällegen Artikel ausgewielt ginn :(",
|
||||
"404-page-title": "Inhalt net fonnt",
|
||||
"404-page-heading": "Net fonnt",
|
||||
"new-404-page-title": "Säit net fonnt",
|
||||
"new-404-page-heading": "Ups. Säit net fonnt.",
|
||||
"404-img-text": "Net fonnt!",
|
||||
"500-page-title": "Interne Feeler um Server",
|
||||
"500-page-heading": "Interne Feeler um Server",
|
||||
"500-page-heading": "Ups. D'Säit funktionéiert net.",
|
||||
"500-page-text": "Et ass en interne Serverfeeler opgetrueden. Mir entschëllegen eis dofir :/",
|
||||
"500-img-text": "Säit funktionéiert net",
|
||||
"external-link-detected": "Externe Link entdeckt",
|
||||
"caution-warning": "Opgepasst!",
|
||||
"fulltext-search-unavailable": "Volltext-Sich net verfügbar",
|
||||
"home-button-text": "Gitt op d'Haaptsäit vun '{{BOOK_TITLE}}'",
|
||||
"search-results-page-title": "Sichen: {{SEARCH_PATTERN}}",
|
||||
"search-results-page-header": "Resultater <b>{{START}}-{{END}}</b> vu(n) <b>{{COUNT}}</b> fir <b>„{{{SEARCH_PATTERN}}}“</b>",
|
||||
"empty-search-results-page-header": "Keng Resultater fonnt fir <b>„{{{SEARCH_PATTERN}}}“</b>",
|
||||
"search-result-book-info": "aus {{BOOK_TITLE}}",
|
||||
"word-count": "{{COUNT}} Wierder",
|
||||
"home-button-text": "Op d'Haaptsäit vun '{{{BOOK_TITLE}}}' goen",
|
||||
"random-page-button-text": "Gitt op eng zoufälleg gewielte Säit",
|
||||
"searchbox-tooltip": "No '{{BOOK_TITLE}}' sichen",
|
||||
"searchbox-tooltip": "No '{{{BOOK_TITLE}}}' sichen",
|
||||
"welcome-page-overzealous-filter": "Kee Resultat. Wëllt Dir <a href=\"{{URL}}\">de Filter zrécksetzen</a>?",
|
||||
"search": "Sichen",
|
||||
"book-filtering-all-categories": "All Kategorien",
|
||||
@@ -24,5 +40,18 @@
|
||||
"count-of-matching-books": "{{COUNT}} Buch/Bicher",
|
||||
"download": "Eroflueden",
|
||||
"direct-download-link-text": "Direkt",
|
||||
"download-links-title": "Buch eroflueden"
|
||||
"torrent-download-link-text": "BitTorrent",
|
||||
"download-links-title": "Buch eroflueden",
|
||||
"unknown-error": "Onbekannte Feeler",
|
||||
"book-category.stack_exchange": "Stack Exchange",
|
||||
"book-category.wikibooks": "Wikibooks",
|
||||
"book-category.wikinews": "Wikinews",
|
||||
"book-category.wikipedia": "Wikipedia",
|
||||
"book-category.wikiquote": "Wikiquote",
|
||||
"book-category.wikisource": "Wikisource",
|
||||
"book-category.wikispecies": "Wikispecies",
|
||||
"book-category.wikiversity": "Wikiversity",
|
||||
"book-category.wikivoyage": "Wikivoyage",
|
||||
"book-category.wiktionary": "Wiktionnaire",
|
||||
"book-category.other": "Anerer"
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"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}}",
|
||||
@@ -21,14 +22,36 @@
|
||||
"400-page-heading": "Неважечко барање",
|
||||
"404-page-title": "Содржината не е најдена",
|
||||
"404-page-heading": "Не е најдено",
|
||||
"new-404-page-title": "Страницата не е пронајдена",
|
||||
"new-404-page-heading": "Ах! Страницата не е пронајдена.",
|
||||
"404-img-text": "Не е најдено!",
|
||||
"path-was-not-found": "Не ја најдов побараната патека:",
|
||||
"404-advice.p1": "Содржината што ја барате може сепак да е достапна, но може да се наоѓа на друго место во рамките на ZIM-податотеката.",
|
||||
"404-advice.p2": "Ве молиме:",
|
||||
"404-advice.p3": "Пробајте да ја употребите функцијата за пребарување за да ја најдете содржината што ви треба",
|
||||
"404-advice.p4": "Барајте клучни зборови или наслови поврзани со информациите што ви требаат",
|
||||
"404-advice.p5": "Овој приод треба да ви помогне да ја најдете саканата содржина, дури и ако изворната врска не работи правилно.",
|
||||
"500-page-title": "Внатрешна грешка во опслужувачот",
|
||||
"500-page-heading": "Внатрешна грешка во опслужувачот",
|
||||
"500-page-heading": "Страницата не работи.",
|
||||
"500-page-text": "Побараната патека не може правилно да се достави:",
|
||||
"500-img-text": "Страницата не работи.",
|
||||
"external-link-detected": "Најдена е надворешна врска",
|
||||
"caution-warning": "Внимание!",
|
||||
"external-link-intro": "На пат сте да го напуштите ZIM-читачот на Кивикс за да појдете на",
|
||||
"external-link-advice.p1": "Врската што пробувате да ја отворите не е дел од вашиот вонмрежен пакет и бара семрежна врска.",
|
||||
"external-link-advice.p2": "Ако можете да се поврзете со семрежнето, пробајте да ја отворите врската.",
|
||||
"external-link-advice.p3": "Во спротивно можете повторно да пробате да ја отворите вонмрежната содржина на вашиот ZIM стискајќи на копчето за враќање назад на вашиот прелистувач.",
|
||||
"fulltext-search-unavailable": "Целотекстното пребарување е недостапно",
|
||||
"no-search-results": "Погонот за целотекстно пребарување не е достапен за оваа содржина.",
|
||||
"search-results-page-title": "Пребарување: {{SEARCH_PATTERN}}",
|
||||
"search-results-page-header": "Исход <b>{{START}}-{{END}}</b> од <b>{{COUNT}}</b> за <b>„{{{SEARCH_PATTERN}}}“</b>",
|
||||
"empty-search-results-page-header": "Не најдов ништо за <b>„{{{SEARCH_PATTERN}}}“</b>",
|
||||
"search-result-book-info": "од {{BOOK_TITLE}}",
|
||||
"word-count": "{{COUNT}} зборови",
|
||||
"library-button-text": "Оди на воведната страница",
|
||||
"home-button-text": "Оди на главната страница на „{{BOOK_TITLE}}“",
|
||||
"home-button-text": "Оди на главната страница на „{{{BOOK_TITLE}}}“",
|
||||
"random-page-button-text": "Оди на случајно избрана страница",
|
||||
"searchbox-tooltip": "Пребарај го „{{BOOK_TITLE}}“",
|
||||
"searchbox-tooltip": "Пребарај по „{{{BOOK_TITLE}}}“",
|
||||
"confusion-of-tongues": "Во пребарувањето ќе учествуваат две или повеќе книги на различни јазици, што може да довете до збунувачки исход.",
|
||||
"welcome-page-overzealous-filter": "Нема исход. Дали би сакале да го <a href=\"{{URL}}\">поништите филтерот</a>?",
|
||||
"powered-by-kiwix-html": "Овозможено од <a href=\"https://kiwix.org\">Кивикс</a>",
|
||||
@@ -38,19 +61,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": "Непосредно преземање преку HTTP(S)",
|
||||
"hash-download-link-text": "Контролен збир Sha256",
|
||||
"hash-download-alt-text": "Прикажи контролен збир SHA-256 на податотеката",
|
||||
"magnet-link-text": "Магнетна врска",
|
||||
"magnet-alt-text": "преземи магнет",
|
||||
"torrent-download-link-text": "Торентна податотека",
|
||||
"torrent-download-alt-text": "преземи торент",
|
||||
"magnet-alt-text": "Преземи преку Magnet-врска",
|
||||
"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": "Преглед"
|
||||
"preview-book": "Преглед",
|
||||
"unknown-error": "Непозната грешка",
|
||||
"book-category.gutenberg": "Гутенберг",
|
||||
"book-category.iFixit": "iFixit",
|
||||
"book-category.mooc": "MOOC",
|
||||
"book-category.phet": "Phet",
|
||||
"book-category.stack_exchange": "Stack Exchange",
|
||||
"book-category.ted": "Ted",
|
||||
"book-category.vikidia": "Викидија",
|
||||
"book-category.wikibooks": "Викикниги",
|
||||
"book-category.wikihow": "wikiHow",
|
||||
"book-category.wikinews": "Викивести",
|
||||
"book-category.wikipedia": "Википедија",
|
||||
"book-category.wikiquote": "Викицитат",
|
||||
"book-category.wikisource": "Викиизвор",
|
||||
"book-category.wikispecies": "Викивидови",
|
||||
"book-category.wikiversity": "Викиуниверзитет",
|
||||
"book-category.wikivoyage": "Википатување",
|
||||
"book-category.wiktionary": "Викиречник",
|
||||
"book-category.other": "друго",
|
||||
"text-loading-content": "Ја вчитувам содржината"
|
||||
}
|
||||
|
||||
74
static/skin/i18n/nb.json
Normal file
74
static/skin/i18n/nb.json
Normal 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"
|
||||
}
|
||||
@@ -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",
|
||||
@@ -22,14 +25,36 @@
|
||||
"400-page-heading": "Ongeldig verzoek",
|
||||
"404-page-title": "Inhoud niet gevonden",
|
||||
"404-page-heading": "Niet gevonden",
|
||||
"new-404-page-title": "Pagina niet gevonden",
|
||||
"new-404-page-heading": "Oeps. Pagina niet gevonden.",
|
||||
"404-img-text": "Niet gevonden!",
|
||||
"path-was-not-found": "Het gevraagde pad is niet gevonden:",
|
||||
"404-advice.p1": "De inhoud die u zoekt is mogelijk toch beschikbaar, maar kan zich ergens anders in het ZIM-bestand bevinden.",
|
||||
"404-advice.p2": "U kunt:",
|
||||
"404-advice.p3": "De zoekfunctie proberen om de gewenste inhoud te vinden",
|
||||
"404-advice.p4": "Trefwoorden of titels opzoeken die verband houden met de informatie die u zoekt",
|
||||
"404-advice.p5": "Met deze aanpak kunt u hopelijk de gewenste inhoud vinden, ook als de oorspronkelijke koppeling niet goed werkt.",
|
||||
"500-page-title": "Interne serverfout",
|
||||
"500-page-heading": "Interne serverfout",
|
||||
"500-page-heading": "Oeps. De pagina werkt niet.",
|
||||
"500-page-text": "Het aangevraagde pad kan niet goed beschikbaar worden gesteld:",
|
||||
"500-img-text": "Pagina werkt niet",
|
||||
"external-link-detected": "Externe koppeling gedetecteerd",
|
||||
"caution-warning": "Voorzichtig!",
|
||||
"external-link-intro": "U staat op het punt de ZIM-lezer van Kiwix te verlaten om online te gaan naar",
|
||||
"external-link-advice.p1": "De koppeling die u probeert te openen, maakt geen deel uit van uw offline-pakket en vereist een internetverbinding.",
|
||||
"external-link-advice.p2": "Als u online kunt gaan, kunt u proberen de koppeling te openen.",
|
||||
"external-link-advice.p3": "Anders kunt u met de terugknop van uw browser terugkeren naar de offline-inhoud van uw ZIM.",
|
||||
"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}}’",
|
||||
"home-button-text": "Naar de hoofdpagina van ‘{{{BOOK_TITLE}}}’",
|
||||
"random-page-button-text": "Naar een willekeurig geselecteerde pagina gaan",
|
||||
"searchbox-tooltip": "Naar ‘{{BOOK_TITLE}}’ zoeken",
|
||||
"searchbox-tooltip": "‘{{{BOOK_TITLE}}}’ doorzoeken",
|
||||
"confusion-of-tongues": "Er zouden twee of meer boeken in verschillende talen deelnemen aan de zoekopdracht, wat tot verwarrende resultaten kan leiden.",
|
||||
"welcome-page-overzealous-filter": "Geen resultaat. Wilt u <a href=\"{{URL}}\">het filter resetten</a>?",
|
||||
"powered-by-kiwix-html": "Mogelijk gemaakt door <a href=\"https://kiwix.org\">Kiwix</a>",
|
||||
@@ -39,19 +64,31 @@
|
||||
"count-of-matching-books": "{{COUNT}} boek(en)",
|
||||
"download": "Downloaden",
|
||||
"direct-download-link-text": "Direct",
|
||||
"direct-download-alt-text": "directe download",
|
||||
"hash-download-link-text": "SHA256-hash",
|
||||
"hash-download-alt-text": "controlesom (hash) van de download",
|
||||
"direct-download-alt-text": "Direct downloaden via HTTP(S)",
|
||||
"hash-download-link-text": "SHA-256-controlesom",
|
||||
"hash-download-alt-text": "De SHA-256-controlesom van het bestand weergeven",
|
||||
"magnet-link-text": "Magnet-link",
|
||||
"magnet-alt-text": "magnet-link van de download",
|
||||
"torrent-download-link-text": "Torrent-bestand",
|
||||
"torrent-download-alt-text": "torrent downloaden",
|
||||
"magnet-alt-text": "Downloaden via Magnet-link",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Amire80",
|
||||
"Lancine.kounfantoh.fofana"
|
||||
]
|
||||
},
|
||||
|
||||
@@ -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}}",
|
||||
|
||||
@@ -25,7 +25,15 @@
|
||||
"home-button-text": "Przejdź do głównej strony '{{BOOK_TITLE}}'",
|
||||
"random-page-button-text": "Przejdź do losowo wybranej strony",
|
||||
"searchbox-tooltip": "Szukaj '{{BOOK_TITLE}}'",
|
||||
"search": "Szukaj",
|
||||
"book-filtering-all-categories": "Wszystkie Kategorie",
|
||||
"book-filtering-all-languages": "Wszystkie języki",
|
||||
"download": "Pobierz",
|
||||
"direct-download-link-text": "Bezpośrednio",
|
||||
"direct-download-alt-text": "bezpośrednie pobieranie",
|
||||
"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"
|
||||
}
|
||||
|
||||
76
static/skin/i18n/pt-br.json
Normal file
76
static/skin/i18n/pt-br.json
Normal file
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Eduardoaddad",
|
||||
"Klgor1803",
|
||||
"Obiru",
|
||||
"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",
|
||||
"404-page-heading": "Não encontrado",
|
||||
"500-page-title": "Erro interno do servidor",
|
||||
"500-page-heading": "Erro interno do servidor",
|
||||
"500-page-text": "Aconteceu um erro interno do servidor. Nós pedimos desculpas sobre isso :/",
|
||||
"caution-warning": "Cuidado!",
|
||||
"fulltext-search-unavailable": "Busca por texto completo está indisponível",
|
||||
"no-search-results": "O motor de busca de texto completo não está disponível para este conteúdo.",
|
||||
"search-results-page-title": "Buscar: {{SEARCH_PATTERN}}",
|
||||
"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>",
|
||||
"search-result-book-info": "de {{BOOK_TITLE}}",
|
||||
"word-count": "{{COUNT}} palavras",
|
||||
"library-button-text": "Ir para página inicial",
|
||||
"home-button-text": "Ir para página principal de '{{BOOK_TITLE}}'",
|
||||
"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",
|
||||
"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",
|
||||
"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
73
static/skin/i18n/pt.json
Normal 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"
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Amire80",
|
||||
"Matthieu Gautier",
|
||||
"Veloman Yunkan",
|
||||
"Verdy p"
|
||||
@@ -15,6 +16,7 @@
|
||||
"suggest-search": "Suggest a search when the URL points to a non existing article",
|
||||
"random-article-failure": "Failure of the random article selection procedure",
|
||||
"invalid-raw-data-type": "Invalid DATATYPE was used with the /raw endpoint (/raw/<book>/DATATYPE/...); allowed values are 'meta' and 'content'",
|
||||
"invalid-request": "Error text for malformed URLs.",
|
||||
"no-value-for-arg": "Error text when no value has been provided for ARGUMENT in the request's query string",
|
||||
"no-query": "Error text when no query has been provided for fulltext search",
|
||||
"raw-entry-not-found": "Entry requested via the /raw endpoint was not found",
|
||||
@@ -22,10 +24,32 @@
|
||||
"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",
|
||||
"500-img-text": "Fallback text for the image on the 500 error page",
|
||||
"external-link-detected": "Title & heading of the external link blocker page",
|
||||
"caution-warning": "Warning of action that shouldn't be carried out carelessly",
|
||||
"external-link-intro": "Message introducing the external link (to be followed by the actual link)",
|
||||
"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",
|
||||
"search-results-page-header": "Header of the search results page",
|
||||
"empty-search-results-page-header": "Header of the empty search results page",
|
||||
"search-result-book-info": "Reference to the book where the search result belongs (this is displayed AFTER the search result)",
|
||||
"word-count": "Word count information",
|
||||
"library-button-text": "Tooltip of the button leading to the welcome page",
|
||||
"home-button-text": "Tooltip of the button leading to the main page of a book",
|
||||
"random-page-button-text": "Tooltip of the button opening a randomly selected page",
|
||||
@@ -52,5 +76,26 @@
|
||||
"welcome-to-kiwix-server": "Title shown in browser's title bar/page tab",
|
||||
"download-links-heading": "Heading for no-js download page",
|
||||
"download-links-title": "Title for no-js download page",
|
||||
"preview-book": "Tooltip of book-tile leading to the book"
|
||||
"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",
|
||||
"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
75
static/skin/i18n/ro.json
Normal 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 <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"
|
||||
}
|
||||
@@ -3,9 +3,11 @@
|
||||
"authors": [
|
||||
"Fenixs-ru",
|
||||
"Kareyac",
|
||||
"Lutece398",
|
||||
"Okras",
|
||||
"Pacha Tchernof",
|
||||
"Razno0",
|
||||
"Rofiatmustapha12",
|
||||
"Smavrina"
|
||||
]
|
||||
},
|
||||
@@ -18,6 +20,7 @@
|
||||
"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": "Не удаётся найти запись {{ENTRY}} типа {{DATATYPE}}",
|
||||
@@ -27,30 +30,51 @@
|
||||
"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>{{START}}-{{END}}</b> из <b>{{COUNT}}</b> для <b>\"{{{SEARCH_PATTERN}}}\"</b>",
|
||||
"empty-search-results-page-header": "По запросу <b>\"{{{SEARCH_PATTERN}}}\"</b> результатов не найдено.",
|
||||
"search-result-book-info": "из {{BOOK_TITLE}}",
|
||||
"word-count": "{{COUNT}} слов",
|
||||
"library-button-text": "Перейти на страницу-приветствие",
|
||||
"home-button-text": "Перейти на главную страницу '{{BOOK_TITLE}}'",
|
||||
"random-page-button-text": "Перейти на случайно выбранную страницу",
|
||||
"searchbox-tooltip": "Искать '{{BOOK_TITLE}}'",
|
||||
"confusion-of-tongues": "В поиске будут участвовать две или более книг на разных языках, что может привести к запутанным результатам.",
|
||||
"welcome-page-overzealous-filter": "Безрезультатно. Хотите <a href=\"{{URL}}\">сбросить фильтр</a> ?",
|
||||
"powered-by-kiwix-html": "При поддержке <a href=\"https://kiwix.org\">Kiwix</a>",
|
||||
"search": "Найти",
|
||||
"book-filtering-all-categories": "Все категории",
|
||||
"book-filtering-all-languages": "Все языки",
|
||||
"count-of-matching-books": "{{COUNT}} книг(и)",
|
||||
"download": "Скачать",
|
||||
"direct-download-alt-text": "прямая загрузка",
|
||||
"hash-download-link-text": "Хэш Sha256",
|
||||
"hash-download-alt-text": "скачать хэш",
|
||||
"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": "Магнитная ссылка",
|
||||
"torrent-download-link-text": "Торрент-файл",
|
||||
"torrent-download-alt-text": "скачать торрент",
|
||||
"magnet-alt-text": "Скачать по Magnet-ссылке",
|
||||
"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": "Предпросмотр"
|
||||
"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": "Другое"
|
||||
}
|
||||
|
||||
@@ -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>",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user