From a18dd82d82101cea9ec6f10d1fb1b560f6d8aebf Mon Sep 17 00:00:00 2001
From: Veloman Yunkan
Date: Thu, 24 Mar 2022 19:12:16 +0400
Subject: [PATCH 01/28] Introduced makeFulltextSearchSuggestion() helper
---
src/server/internalServer.cpp | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/src/server/internalServer.cpp b/src/server/internalServer.cpp
index 8d1b5027a..04864142e 100644
--- a/src/server/internalServer.cpp
+++ b/src/server/internalServer.cpp
@@ -442,6 +442,11 @@ SuggestionsList_t getSuggestions(SuggestionSearcherCache& cache, const zim::Arch
namespace
{
+std::string makeFulltextSearchSuggestion(const std::string& queryString)
+{
+ return "containing '" + queryString + "'...";
+}
+
std::string noSuchBookErrorMsg(const std::string& bookName)
{
return "No such book: " + bookName;
@@ -514,7 +519,7 @@ std::unique_ptr InternalServer::handle_suggest(const RequestContext& r
/* Propose the fulltext search if possible */
if (archive->hasFulltextIndex()) {
MustacheData result;
- result.set("label", "containing '" + queryString + "'...");
+ result.set("label", makeFulltextSearchSuggestion(queryString));
result.set("value", queryString + " ");
result.set("kind", "pattern");
result.set("first", first);
From c574735f515f18cf846a0d4df2f7ee879c198ec8 Mon Sep 17 00:00:00 2001
From: Veloman Yunkan
Date: Sun, 16 Jan 2022 18:02:52 +0400
Subject: [PATCH 02/28] makeFulltextSearchSuggestion() works via mustache
---
src/server/internalServer.cpp | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/src/server/internalServer.cpp b/src/server/internalServer.cpp
index 04864142e..3dda112bd 100644
--- a/src/server/internalServer.cpp
+++ b/src/server/internalServer.cpp
@@ -444,7 +444,13 @@ namespace
std::string makeFulltextSearchSuggestion(const std::string& queryString)
{
- return "containing '" + queryString + "'...";
+ MustacheData data;
+ data.set("SEARCH_TERMS", queryString);
+ // NOTE: Search terms are **not** HTML-escaped at this point.
+ // NOTE: HTML-escaping is performed when the result of this function
+ // NOTE: is expanded into the suggestions.json template
+ const std::string tmpl("containing '{{{SEARCH_TERMS}}}'...");
+ return render_template(tmpl, data);
}
std::string noSuchBookErrorMsg(const std::string& bookName)
From d029c2b8d524764a11c450dc850d801cd6138fe9 Mon Sep 17 00:00:00 2001
From: Veloman Yunkan
Date: Sun, 16 Jan 2022 19:16:14 +0400
Subject: [PATCH 03/28] Enter I18nStringDB
---
src/meson.build | 1 +
src/server/i18n.cpp | 98 +++++++++++++++++++++++++++++++++++
src/server/i18n.h | 45 ++++++++++++++++
src/server/internalServer.cpp | 6 +--
4 files changed, 146 insertions(+), 4 deletions(-)
create mode 100644 src/server/i18n.cpp
create mode 100644 src/server/i18n.h
diff --git a/src/meson.build b/src/meson.build
index 545d1bc99..9a5d4ff56 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -28,6 +28,7 @@ kiwix_sources = [
'server/response.cpp',
'server/internalServer.cpp',
'server/internalServer_catalog_v2.cpp',
+ 'server/i18n.cpp',
'opds_catalog.cpp',
'version.cpp'
]
diff --git a/src/server/i18n.cpp b/src/server/i18n.cpp
new file mode 100644
index 000000000..2e59ea60e
--- /dev/null
+++ b/src/server/i18n.cpp
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2022 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 "i18n.h"
+
+#include
+#include
+
+namespace kiwix
+{
+
+const char* I18nStringTable::get(const std::string& key) const
+{
+ const I18nString* const begin = entries;
+ const I18nString* const end = begin + entryCount;
+ const I18nString* found = std::lower_bound(begin, end, key,
+ [](const I18nString& a, const std::string& k) {
+ return a.key < k;
+ });
+ return (found == end || found->key != key) ? nullptr : found->value;
+}
+
+namespace
+{
+
+const I18nString enStrings[] = {
+ // must be sorted by key
+ { "suggest-full-text-search", "containing '{{{SEARCH_TERMS}}}'..."}
+};
+
+#define ARRAY_ELEMENT_COUNT(a) (sizeof(a)/sizeof(a[0]))
+
+const I18nStringTable i18nStringTables[] = {
+ { "en", ARRAY_ELEMENT_COUNT(enStrings), enStrings }
+};
+
+class I18nStringDB
+{
+public: // functions
+ I18nStringDB() {
+ for ( size_t i = 0; i < ARRAY_ELEMENT_COUNT(i18nStringTables); ++i ) {
+ const auto& t = i18nStringTables[i];
+ lang2TableMap[t.lang] = &t;
+ }
+ enStrings = lang2TableMap.at("en");
+ };
+
+ std::string get(const std::string& lang, const std::string& key) const {
+ const char* s = getStringsFor(lang)->get(key);
+ if ( s == nullptr ) {
+ s = enStrings->get(key);
+ if ( s == nullptr ) {
+ throw std::runtime_error("Invalid message id");
+ }
+ }
+ return s;
+ }
+
+private: // functions
+ const I18nStringTable* getStringsFor(const std::string& lang) const {
+ try {
+ return lang2TableMap.at(lang);
+ } catch(const std::out_of_range&) {
+ return enStrings;
+ }
+ }
+
+private: // data
+ std::map lang2TableMap;
+ const I18nStringTable* enStrings;
+};
+
+} // unnamed namespace
+
+std::string getTranslatedString(const std::string& lang, const std::string& key)
+{
+ static const I18nStringDB stringDb;
+
+ return stringDb.get(lang, key);
+}
+
+} // namespace kiwix
diff --git a/src/server/i18n.h b/src/server/i18n.h
new file mode 100644
index 000000000..ffc2341e5
--- /dev/null
+++ b/src/server/i18n.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2022 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_SERVER_I18N
+#define KIWIX_SERVER_I18N
+
+#include
+
+namespace kiwix
+{
+
+struct I18nString {
+ const char* const key;
+ const char* const value;
+};
+
+struct I18nStringTable {
+ const char* const lang;
+ const size_t entryCount;
+ const I18nString* const entries;
+
+ const char* get(const std::string& key) const;
+};
+
+std::string getTranslatedString(const std::string& lang, const std::string& key);
+
+} // namespace kiwix
+
+#endif // KIWIX_SERVER_I18N
diff --git a/src/server/internalServer.cpp b/src/server/internalServer.cpp
index 3dda112bd..81c7f8a73 100644
--- a/src/server/internalServer.cpp
+++ b/src/server/internalServer.cpp
@@ -55,6 +55,7 @@ extern "C" {
#include "searcher.h"
#include "search_renderer.h"
#include "opds_dumper.h"
+#include "i18n.h"
#include
#include
@@ -446,10 +447,7 @@ std::string makeFulltextSearchSuggestion(const std::string& queryString)
{
MustacheData data;
data.set("SEARCH_TERMS", queryString);
- // NOTE: Search terms are **not** HTML-escaped at this point.
- // NOTE: HTML-escaping is performed when the result of this function
- // NOTE: is expanded into the suggestions.json template
- const std::string tmpl("containing '{{{SEARCH_TERMS}}}'...");
+ const std::string tmpl = getTranslatedString("en", "suggest-full-text-search");
return render_template(tmpl, data);
}
From 507e111f3488cd9fdbf9c8a44a962ec90c7d280b Mon Sep 17 00:00:00 2001
From: Veloman Yunkan
Date: Sun, 16 Jan 2022 23:33:44 +0400
Subject: [PATCH 04/28] i18n data is kept in and generated from JSON files
Introduced a new resource compiler script kiwix-compile-i18n that
processes i18n string data stored in JSON files and generates sorted C++
tables of string keys and values for all languages.
---
debian/libkiwix-dev.manpages | 1 +
scripts/kiwix-compile-i18n | 161 ++++++++++++++++++++++++++++++++
scripts/kiwix-compile-i18n.1 | 18 ++++
scripts/kiwix-compile-resources | 2 +-
scripts/meson.build | 6 ++
src/meson.build | 1 +
src/server/i18n.cpp | 22 ++---
static/i18n/en.json | 8 ++
static/i18n/qqq.json | 9 ++
static/i18n_resources_list.txt | 1 +
static/meson.build | 15 +++
11 files changed, 230 insertions(+), 14 deletions(-)
create mode 100755 scripts/kiwix-compile-i18n
create mode 100644 scripts/kiwix-compile-i18n.1
create mode 100644 static/i18n/en.json
create mode 100644 static/i18n/qqq.json
create mode 100644 static/i18n_resources_list.txt
diff --git a/debian/libkiwix-dev.manpages b/debian/libkiwix-dev.manpages
index 66a5c0345..86e96e595 100644
--- a/debian/libkiwix-dev.manpages
+++ b/debian/libkiwix-dev.manpages
@@ -1 +1,2 @@
usr/share/man/man1/kiwix-compile-resources.1*
+usr/share/man/man1/kiwix-compile-i18n.1*
diff --git a/scripts/kiwix-compile-i18n b/scripts/kiwix-compile-i18n
new file mode 100755
index 000000000..fbbf8d903
--- /dev/null
+++ b/scripts/kiwix-compile-i18n
@@ -0,0 +1,161 @@
+#!/usr/bin/env python3
+
+'''
+Copyright 2022 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.
+'''
+
+import argparse
+import os.path
+import re
+import json
+
+def to_identifier(name):
+ ident = re.sub(r'[^0-9a-zA-Z]', '_', name)
+ if ident[0].isnumeric():
+ return "_"+ident
+ return ident
+
+def lang_code(filename):
+ filename = os.path.basename(filename)
+ lang = to_identifier(os.path.splitext(filename)[0])
+ print(filename, '->', lang)
+ return lang
+
+from string import Template
+
+def expand_cxx_template(t, **kwargs):
+ return Template(t).substitute(**kwargs)
+
+def cxx_string_literal(s):
+ # Taking advantage of the fact the JSON string escape rules match
+ # those of C++
+ return 'u8' + json.dumps(s)
+
+string_table_cxx_template = '''
+const I18nString $TABLE_NAME[] = {
+ $TABLE_ENTRIES
+};
+'''
+
+lang_table_entry_cxx_template = '''
+ {
+ $LANG_STRING_LITERAL,
+ ARRAY_ELEMENT_COUNT($STRING_TABLE_NAME),
+ $STRING_TABLE_NAME
+ }'''
+
+cxxfile_template = '''// This file is automatically generated. Do not modify it.
+
+#include "server/i18n.h"
+
+namespace kiwix {
+namespace i18n {
+
+namespace
+{
+
+$STRING_DATA
+
+} // unnamed namespace
+
+#define ARRAY_ELEMENT_COUNT(a) (sizeof(a)/sizeof(a[0]))
+
+extern const I18nStringTable stringTables[] = {
+ $LANG_TABLE
+};
+
+extern const size_t langCount = $LANG_COUNT;
+
+} // namespace i18n
+} // namespace kiwix
+'''
+
+class Resource:
+ def __init__(self, base_dirs, filename):
+ filename = filename.strip()
+ self.filename = filename
+ self.lang_code = lang_code(filename)
+ found = False
+ for base_dir in base_dirs:
+ try:
+ with open(os.path.join(base_dir, filename), 'r') as f:
+ self.data = f.read()
+ found = True
+ break
+ except FileNotFoundError:
+ continue
+ if not found:
+ raise Exception("Impossible to find {}".format(filename))
+
+
+ def get_string_table_name(self):
+ return "string_table_for_" + self.lang_code
+
+ def get_string_table(self):
+ table_entries = ",\n ".join(self.get_string_table_entries())
+ return expand_cxx_template(string_table_cxx_template,
+ TABLE_NAME=self.get_string_table_name(),
+ TABLE_ENTRIES=table_entries)
+
+ def get_string_table_entries(self):
+ d = json.loads(self.data)
+ for k in sorted(d.keys()):
+ if k != "@metadata":
+ key_string = cxx_string_literal(k)
+ value_string = cxx_string_literal(d[k])
+ yield '{ ' + key_string + ', ' + value_string + ' }'
+
+ def get_lang_table_entry(self):
+ return expand_cxx_template(lang_table_entry_cxx_template,
+ LANG_STRING_LITERAL=cxx_string_literal(self.lang_code),
+ STRING_TABLE_NAME=self.get_string_table_name())
+
+
+
+def gen_c_file(resources):
+ string_data = []
+ lang_table = []
+ for r in resources:
+ string_data.append(r.get_string_table())
+ lang_table.append(r.get_lang_table_entry())
+
+ return expand_cxx_template(cxxfile_template,
+ STRING_DATA="\n".join(string_data),
+ LANG_TABLE=",\n ".join(lang_table),
+ LANG_COUNT=len(resources)
+ )
+
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--cxxfile',
+ required=True,
+ help='The Cpp file name to generate')
+ parser.add_argument('i18n_resource_file',
+ help='The list of resources to compile.')
+ args = parser.parse_args()
+
+ base_dir = os.path.dirname(os.path.realpath(args.i18n_resource_file))
+ with open(args.i18n_resource_file, 'r') as f:
+ resources = [Resource([base_dir], filename)
+ for filename in f.readlines()]
+
+ with open(args.cxxfile, 'w') as f:
+ f.write(gen_c_file(resources))
+
diff --git a/scripts/kiwix-compile-i18n.1 b/scripts/kiwix-compile-i18n.1
new file mode 100644
index 000000000..383ae83b9
--- /dev/null
+++ b/scripts/kiwix-compile-i18n.1
@@ -0,0 +1,18 @@
+.TH KIWIX-COMPILE-I18N "1" "January 2022" "Kiwix" "User Commands"
+.SH NAME
+kiwix-compile-i18n \- helper to compile Kiwix i18n (internationalization) data
+.SH SYNOPSIS
+\fBkiwix\-compile\-i18n\fR [\-h] \-\-cxxfile CXXFILE i18n_resource_file\fR
+.SH DESCRIPTION
+.TP
+i18n_resource_file
+The list of i18n resources to compile.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+show a help message and exit
+.TP
+\fB\-\-cxxfile\fR CXXFILE
+The Cpp file name to generate
+.TP
+.SH AUTHOR
+Veloman Yunkan
diff --git a/scripts/kiwix-compile-resources b/scripts/kiwix-compile-resources
index 265db5ff6..7c1bf3a8a 100755
--- a/scripts/kiwix-compile-resources
+++ b/scripts/kiwix-compile-resources
@@ -102,7 +102,7 @@ class Resource:
-master_c_template = """//This file is automaically generated. Do not modify it.
+master_c_template = """//This file is automatically generated. Do not modify it.
#include
#include
diff --git a/scripts/meson.build b/scripts/meson.build
index fb0f9cb97..c4ddec873 100644
--- a/scripts/meson.build
+++ b/scripts/meson.build
@@ -4,3 +4,9 @@ res_compiler = find_program('kiwix-compile-resources')
install_data(res_compiler.path(), install_dir:get_option('bindir'))
install_man('kiwix-compile-resources.1')
+
+i18n_compiler = find_program('kiwix-compile-i18n')
+
+install_data(i18n_compiler.path(), install_dir:get_option('bindir'))
+
+install_man('kiwix-compile-i18n.1')
diff --git a/src/meson.build b/src/meson.build
index 9a5d4ff56..c9c69445d 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -33,6 +33,7 @@ kiwix_sources = [
'version.cpp'
]
kiwix_sources += lib_resources
+kiwix_sources += i18n_resources
if host_machine.system() == 'windows'
kiwix_sources += 'subprocess_windows.cpp'
diff --git a/src/server/i18n.cpp b/src/server/i18n.cpp
index 2e59ea60e..f06c415b2 100644
--- a/src/server/i18n.cpp
+++ b/src/server/i18n.cpp
@@ -36,26 +36,22 @@ const char* I18nStringTable::get(const std::string& key) const
return (found == end || found->key != key) ? nullptr : found->value;
}
+namespace i18n
+{
+// this data is generated by the i18n resource compiler
+extern const I18nStringTable stringTables[];
+extern const size_t langCount;
+}
+
namespace
{
-const I18nString enStrings[] = {
- // must be sorted by key
- { "suggest-full-text-search", "containing '{{{SEARCH_TERMS}}}'..."}
-};
-
-#define ARRAY_ELEMENT_COUNT(a) (sizeof(a)/sizeof(a[0]))
-
-const I18nStringTable i18nStringTables[] = {
- { "en", ARRAY_ELEMENT_COUNT(enStrings), enStrings }
-};
-
class I18nStringDB
{
public: // functions
I18nStringDB() {
- for ( size_t i = 0; i < ARRAY_ELEMENT_COUNT(i18nStringTables); ++i ) {
- const auto& t = i18nStringTables[i];
+ for ( size_t i = 0; i < kiwix::i18n::langCount; ++i ) {
+ const auto& t = kiwix::i18n::stringTables[i];
lang2TableMap[t.lang] = &t;
}
enStrings = lang2TableMap.at("en");
diff --git a/static/i18n/en.json b/static/i18n/en.json
new file mode 100644
index 000000000..6a111b289
--- /dev/null
+++ b/static/i18n/en.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ ]
+ },
+ "name":"English",
+ "suggest-full-text-search": "containing '{{{SEARCH_TERMS}}}'..."
+}
diff --git a/static/i18n/qqq.json b/static/i18n/qqq.json
new file mode 100644
index 000000000..f0aaaa8dd
--- /dev/null
+++ b/static/i18n/qqq.json
@@ -0,0 +1,9 @@
+{
+ "@metadata": {
+ "authors": [
+ "Veloman Yunkan"
+ ]
+ },
+ "name": "Current language to which the string is being translated to.",
+ "suggest-full-text-search": "Text appearing in the suggestion list that, when selected, runs a full text search instead of the title search"
+}
diff --git a/static/i18n_resources_list.txt b/static/i18n_resources_list.txt
new file mode 100644
index 000000000..eb8bddb12
--- /dev/null
+++ b/static/i18n_resources_list.txt
@@ -0,0 +1 @@
+i18n/en.json
diff --git a/static/meson.build b/static/meson.build
index 5c6d2899b..6a8ce1773 100644
--- a/static/meson.build
+++ b/static/meson.build
@@ -14,3 +14,18 @@ lib_resources = custom_target('resources',
'@INPUT@'],
depend_files: resource_files
)
+
+i18n_resource_files = run_command(find_program('python3'),
+ '-c',
+ 'import sys; f=open(sys.argv[1]); print(f.read())',
+ files('i18n_resources_list.txt')
+ ).stdout().strip().split('\n')
+
+i18n_resources = custom_target('i18n_resources',
+ input: 'i18n_resources_list.txt',
+ output: ['libkiwix-i18n-resources.cpp'],
+ command:[i18n_compiler,
+ '--cxxfile', '@OUTPUT0@',
+ '@INPUT@'],
+ depend_files: i18n_resource_files
+)
From e4a0a029ff0739c9fcbb0d95b0ec6311d4cf76e8 Mon Sep 17 00:00:00 2001
From: Veloman Yunkan
Date: Mon, 17 Jan 2022 00:28:24 +0400
Subject: [PATCH 05/28] User language control via userlang query param
This is a draft commit enabling the testing of the support for
kiwix-serve internationalization.
---
src/server/internalServer.cpp | 7 ++++---
src/server/request_context.cpp | 5 +++++
src/server/request_context.h | 2 ++
static/i18n/hy.json | 8 ++++++++
static/i18n_resources_list.txt | 1 +
static/skin/taskbar.js | 3 ++-
test/server.cpp | 11 +++++++++++
7 files changed, 33 insertions(+), 4 deletions(-)
create mode 100644 static/i18n/hy.json
diff --git a/src/server/internalServer.cpp b/src/server/internalServer.cpp
index 81c7f8a73..7ecde95af 100644
--- a/src/server/internalServer.cpp
+++ b/src/server/internalServer.cpp
@@ -443,11 +443,11 @@ SuggestionsList_t getSuggestions(SuggestionSearcherCache& cache, const zim::Arch
namespace
{
-std::string makeFulltextSearchSuggestion(const std::string& queryString)
+std::string makeFulltextSearchSuggestion(const std::string& lang, const std::string& queryString)
{
MustacheData data;
data.set("SEARCH_TERMS", queryString);
- const std::string tmpl = getTranslatedString("en", "suggest-full-text-search");
+ const std::string tmpl = getTranslatedString(lang, "suggest-full-text-search");
return render_template(tmpl, data);
}
@@ -523,7 +523,8 @@ std::unique_ptr InternalServer::handle_suggest(const RequestContext& r
/* Propose the fulltext search if possible */
if (archive->hasFulltextIndex()) {
MustacheData result;
- result.set("label", makeFulltextSearchSuggestion(queryString));
+ const auto lang = request.get_user_language();
+ result.set("label", makeFulltextSearchSuggestion(lang, queryString));
result.set("value", queryString + " ");
result.set("kind", "pattern");
result.set("first", first);
diff --git a/src/server/request_context.cpp b/src/server/request_context.cpp
index 765d01adf..5ac9fe8dd 100644
--- a/src/server/request_context.cpp
+++ b/src/server/request_context.cpp
@@ -193,4 +193,9 @@ std::string RequestContext::get_query() const {
return q;
}
+std::string RequestContext::get_user_language() const
+{
+ return get_optional_param("userlang", "en");
+}
+
}
diff --git a/src/server/request_context.h b/src/server/request_context.h
index 5457ae4bf..7bdd7d87c 100644
--- a/src/server/request_context.h
+++ b/src/server/request_context.h
@@ -94,6 +94,8 @@ class RequestContext {
bool can_compress() const { return acceptEncodingDeflate; }
+ std::string get_user_language() const;
+
private: // data
std::string full_url;
std::string url;
diff --git a/static/i18n/hy.json b/static/i18n/hy.json
new file mode 100644
index 000000000..b1963af29
--- /dev/null
+++ b/static/i18n/hy.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ ]
+ },
+ "name":"Հայերեն",
+ "suggest-full-text-search": "որոնել '{{{SEARCH_TERMS}}}'..."
+}
diff --git a/static/i18n_resources_list.txt b/static/i18n_resources_list.txt
index eb8bddb12..d7a04cdb9 100644
--- a/static/i18n_resources_list.txt
+++ b/static/i18n_resources_list.txt
@@ -1 +1,2 @@
i18n/en.json
+i18n/hy.json
diff --git a/static/skin/taskbar.js b/static/skin/taskbar.js
index d2d26fb85..421758216 100644
--- a/static/skin/taskbar.js
+++ b/static/skin/taskbar.js
@@ -12,9 +12,10 @@ jq(document).ready(() => {
? (new URLSearchParams(window.location.search)).get('content')
: window.location.pathname.split(`${root}/`)[1].split('/')[0];
+ const userlang = (new URLSearchParams(window.location.search)).get('userlang') || "en";
$( "#kiwixsearchbox" ).autocomplete({
- source: `${root}/suggest?content=${bookName}`,
+ source: `${root}/suggest?content=${bookName}&userlang=${userlang}`,
dataType: "json",
cache: false,
diff --git a/test/server.cpp b/test/server.cpp
index d4056dda0..d56f22d51 100644
--- a/test/server.cpp
+++ b/test/server.cpp
@@ -1182,6 +1182,17 @@ R"EXPECTEDRESPONSE([
//EOLWHITESPACEMARKER
}
]
+)EXPECTEDRESPONSE"
+ },
+ { /* url: */ "/ROOT/suggest?content=zimfile&term=abracadabra&userlang=hy",
+R"EXPECTEDRESPONSE([
+ {
+ "value" : "abracadabra ",
+ "label" : "որոնել 'abracadabra'...",
+ "kind" : "pattern"
+ //EOLWHITESPACEMARKER
+ }
+]
)EXPECTEDRESPONSE"
},
};
From 577b6e29f9421bdf25dd0fd3a3b6072a5717a79f Mon Sep 17 00:00:00 2001
From: Veloman Yunkan
Date: Thu, 20 Jan 2022 21:25:39 +0400
Subject: [PATCH 06/28] kiwix::i18n::expandParameterizedString()
---
src/server/i18n.cpp | 15 +++++++++++++++
src/server/i18n.h | 12 ++++++++++++
src/server/internalServer.cpp | 9 +++++----
3 files changed, 32 insertions(+), 4 deletions(-)
diff --git a/src/server/i18n.cpp b/src/server/i18n.cpp
index f06c415b2..740df7da1 100644
--- a/src/server/i18n.cpp
+++ b/src/server/i18n.cpp
@@ -19,6 +19,8 @@
#include "i18n.h"
+#include "tools/otherTools.h"
+
#include
#include
@@ -91,4 +93,17 @@ std::string getTranslatedString(const std::string& lang, const std::string& key)
return stringDb.get(lang, key);
}
+namespace i18n
+{
+
+std::string expandParameterizedString(const std::string& lang,
+ const std::string& key,
+ const Parameters& params)
+{
+ const std::string tmpl = getTranslatedString(lang, key);
+ return render_template(tmpl, params);
+}
+
+} // namespace i18n
+
} // namespace kiwix
diff --git a/src/server/i18n.h b/src/server/i18n.h
index ffc2341e5..4f1e645ff 100644
--- a/src/server/i18n.h
+++ b/src/server/i18n.h
@@ -21,6 +21,7 @@
#define KIWIX_SERVER_I18N
#include
+#include
namespace kiwix
{
@@ -40,6 +41,17 @@ struct I18nStringTable {
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);
+
+} // namespace i18n
+
} // namespace kiwix
#endif // KIWIX_SERVER_I18N
diff --git a/src/server/internalServer.cpp b/src/server/internalServer.cpp
index 7ecde95af..be831d5df 100644
--- a/src/server/internalServer.cpp
+++ b/src/server/internalServer.cpp
@@ -445,10 +445,11 @@ namespace
std::string makeFulltextSearchSuggestion(const std::string& lang, const std::string& queryString)
{
- MustacheData data;
- data.set("SEARCH_TERMS", queryString);
- const std::string tmpl = getTranslatedString(lang, "suggest-full-text-search");
- return render_template(tmpl, data);
+ return i18n::expandParameterizedString(lang, "suggest-full-text-search",
+ {
+ {"SEARCH_TERMS", queryString}
+ }
+ );
}
std::string noSuchBookErrorMsg(const std::string& bookName)
From 202ec81d8bc2f03e389d66e9ab413affccb16e59 Mon Sep 17 00:00:00 2001
From: Veloman Yunkan
Date: Thu, 24 Mar 2022 19:37:56 +0400
Subject: [PATCH 07/28] URL-not-found message went into i18n JSON resource
Yet, the URL-not-found message is not yet fully internationalized
since its usage is hardcoded to English.
---
src/server/response.cpp | 9 +++++++--
static/i18n/en.json | 1 +
static/i18n/qqq.json | 1 +
3 files changed, 9 insertions(+), 2 deletions(-)
diff --git a/src/server/response.cpp b/src/server/response.cpp
index f87ffecb5..795ec7dc4 100644
--- a/src/server/response.cpp
+++ b/src/server/response.cpp
@@ -21,6 +21,7 @@
#include "request_context.h"
#include "internalServer.h"
#include "kiwixlib-resources.h"
+#include "i18n.h"
#include "tools/regexTools.h"
#include "tools/stringTools.h"
@@ -131,8 +132,12 @@ HTTP404HtmlResponse::HTTP404HtmlResponse(const InternalServer& server,
HTTPErrorHtmlResponse& HTTP404HtmlResponse::operator+(UrlNotFoundMsg /*unused*/)
{
const std::string requestUrl = m_request.get_full_url();
- kainjow::mustache::mustache msgTmpl(R"(The requested URL "{{url}}" was not found on this server.)");
- return *this + msgTmpl.render({"url", requestUrl});
+ const auto urlNotFoundMsg = i18n::expandParameterizedString(
+ "en", // FIXME: hardcoded language
+ "url-not-found",
+ {{"url", requestUrl}}
+ );
+ return *this + urlNotFoundMsg;
}
HTTPErrorHtmlResponse& HTTPErrorHtmlResponse::operator+(const std::string& msg)
diff --git a/static/i18n/en.json b/static/i18n/en.json
index 6a111b289..4b87abe0a 100644
--- a/static/i18n/en.json
+++ b/static/i18n/en.json
@@ -5,4 +5,5 @@
},
"name":"English",
"suggest-full-text-search": "containing '{{{SEARCH_TERMS}}}'..."
+ , "url-not-found" : "The requested URL \"{{url}}\" was not found on this server."
}
diff --git a/static/i18n/qqq.json b/static/i18n/qqq.json
index f0aaaa8dd..f62e435e1 100644
--- a/static/i18n/qqq.json
+++ b/static/i18n/qqq.json
@@ -6,4 +6,5 @@
},
"name": "Current language to which the string is being translated to.",
"suggest-full-text-search": "Text appearing in the suggestion list that, when selected, runs a full text search instead of the title search"
+ , "url-not-found" : "Error text about wrong URL for an HTTP 404 error"
}
From 387f977d6ce24ec2f88a20103e29b3464690c201 Mon Sep 17 00:00:00 2001
From: Veloman Yunkan
Date: Tue, 29 Mar 2022 20:16:53 +0400
Subject: [PATCH 08/28] Enter ParameterizedMessage
---
src/server/i18n.cpp | 5 +++++
src/server/i18n.h | 18 ++++++++++++++++++
src/server/response.cpp | 14 +++++++-------
src/server/response.h | 2 ++
4 files changed, 32 insertions(+), 7 deletions(-)
diff --git a/src/server/i18n.cpp b/src/server/i18n.cpp
index 740df7da1..2aecc724d 100644
--- a/src/server/i18n.cpp
+++ b/src/server/i18n.cpp
@@ -106,4 +106,9 @@ std::string expandParameterizedString(const std::string& lang,
} // namespace i18n
+std::string ParameterizedMessage::getText(const std::string& lang) const
+{
+ return i18n::expandParameterizedString(lang, msgId, params);
+}
+
} // namespace kiwix
diff --git a/src/server/i18n.h b/src/server/i18n.h
index 4f1e645ff..955f13091 100644
--- a/src/server/i18n.h
+++ b/src/server/i18n.h
@@ -52,6 +52,24 @@ std::string expandParameterizedString(const std::string& lang,
} // 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;
+};
+
} // namespace kiwix
#endif // KIWIX_SERVER_I18N
diff --git a/src/server/response.cpp b/src/server/response.cpp
index 795ec7dc4..09a9b4745 100644
--- a/src/server/response.cpp
+++ b/src/server/response.cpp
@@ -21,7 +21,6 @@
#include "request_context.h"
#include "internalServer.h"
#include "kiwixlib-resources.h"
-#include "i18n.h"
#include "tools/regexTools.h"
#include "tools/stringTools.h"
@@ -132,12 +131,7 @@ HTTP404HtmlResponse::HTTP404HtmlResponse(const InternalServer& server,
HTTPErrorHtmlResponse& HTTP404HtmlResponse::operator+(UrlNotFoundMsg /*unused*/)
{
const std::string requestUrl = m_request.get_full_url();
- const auto urlNotFoundMsg = i18n::expandParameterizedString(
- "en", // FIXME: hardcoded language
- "url-not-found",
- {{"url", requestUrl}}
- );
- return *this + urlNotFoundMsg;
+ return *this + ParameterizedMessage("url-not-found", {{"url", requestUrl}});
}
HTTPErrorHtmlResponse& HTTPErrorHtmlResponse::operator+(const std::string& msg)
@@ -146,6 +140,12 @@ HTTPErrorHtmlResponse& HTTPErrorHtmlResponse::operator+(const std::string& msg)
return *this;
}
+HTTPErrorHtmlResponse& HTTPErrorHtmlResponse::operator+(const ParameterizedMessage& details)
+{
+ return *this + details.getText(m_request.get_user_language());
+}
+
+
HTTP400HtmlResponse::HTTP400HtmlResponse(const InternalServer& server,
const RequestContext& request)
: HTTPErrorHtmlResponse(server,
diff --git a/src/server/response.h b/src/server/response.h
index 1c93434e3..d9a0ada88 100644
--- a/src/server/response.h
+++ b/src/server/response.h
@@ -28,6 +28,7 @@
#include "byte_range.h"
#include "entry.h"
#include "etag.h"
+#include "i18n.h"
extern "C" {
#include "microhttpd_wrapper.h"
@@ -189,6 +190,7 @@ struct HTTPErrorHtmlResponse : ContentResponseBlueprint
using ContentResponseBlueprint::operator+;
HTTPErrorHtmlResponse& operator+(const std::string& msg);
+ HTTPErrorHtmlResponse& operator+(const ParameterizedMessage& errorDetails);
};
class UrlNotFoundMsg {};
From b2526c7a980314369d3614e26d1fc8ef426d8042 Mon Sep 17 00:00:00 2001
From: Veloman Yunkan
Date: Thu, 24 Mar 2022 19:41:18 +0400
Subject: [PATCH 09/28] Translation of the url-not-found message
---
static/i18n/hy.json | 1 +
test/server.cpp | 16 ++++++++++++++++
2 files changed, 17 insertions(+)
diff --git a/static/i18n/hy.json b/static/i18n/hy.json
index b1963af29..376236325 100644
--- a/static/i18n/hy.json
+++ b/static/i18n/hy.json
@@ -5,4 +5,5 @@
},
"name":"Հայերեն",
"suggest-full-text-search": "որոնել '{{{SEARCH_TERMS}}}'..."
+ , "url-not-found" : "Սխալ հասցե՝ {{url}}"
}
diff --git a/test/server.cpp b/test/server.cpp
index d56f22d51..7508a7bf2 100644
--- a/test/server.cpp
+++ b/test/server.cpp
@@ -587,6 +587,14 @@ TEST_F(ServerTest, 404WithBodyTesting)
)" },
+ { /* url */ "/ROOT/catalog/?userlang=hy",
+ expected_body==R"(
+ Not Found
+
+ Սխալ հասցե՝ /ROOT/catalog/
+
+)" },
+
{ /* url */ "/ROOT/catalog/invalid_endpoint",
expected_body==R"(
Not Found
@@ -595,6 +603,14 @@ TEST_F(ServerTest, 404WithBodyTesting)
)" },
+ { /* url */ "/ROOT/catalog/invalid_endpoint?userlang=hy",
+ expected_body==R"(
+ Not Found
+
+ Սխալ հասցե՝ /ROOT/catalog/invalid_endpoint
+
+)" },
+
{ /* url */ "/ROOT/invalid-book/whatever",
expected_body==R"(
Not Found
From cb5ae01fd8ffd2f5c94110064dcbada76ef2179f Mon Sep 17 00:00:00 2001
From: Veloman Yunkan
Date: Thu, 24 Mar 2022 19:33:56 +0400
Subject: [PATCH 10/28] Localized "No such book" 404 message for /random
However the title and the heading of the 404 page are not localized yet.
---
src/server/internalServer.cpp | 4 ++--
static/i18n/en.json | 1 +
static/i18n/hy.json | 1 +
static/i18n/qqq.json | 1 +
test/server.cpp | 8 ++++++++
5 files changed, 13 insertions(+), 2 deletions(-)
diff --git a/src/server/internalServer.cpp b/src/server/internalServer.cpp
index be831d5df..60d8b0741 100644
--- a/src/server/internalServer.cpp
+++ b/src/server/internalServer.cpp
@@ -452,9 +452,9 @@ std::string makeFulltextSearchSuggestion(const std::string& lang, const std::str
);
}
-std::string noSuchBookErrorMsg(const std::string& bookName)
+ParameterizedMessage noSuchBookErrorMsg(const std::string& bookName)
{
- return "No such book: " + bookName;
+ return ParameterizedMessage("no-such-book", { {"BOOK_NAME", bookName} });
}
std::string noSearchResultsMsg()
diff --git a/static/i18n/en.json b/static/i18n/en.json
index 4b87abe0a..d6b197e76 100644
--- a/static/i18n/en.json
+++ b/static/i18n/en.json
@@ -5,5 +5,6 @@
},
"name":"English",
"suggest-full-text-search": "containing '{{{SEARCH_TERMS}}}'..."
+ , "no-such-book": "No such book: {{BOOK_NAME}}"
, "url-not-found" : "The requested URL \"{{url}}\" was not found on this server."
}
diff --git a/static/i18n/hy.json b/static/i18n/hy.json
index 376236325..9b55d8c38 100644
--- a/static/i18n/hy.json
+++ b/static/i18n/hy.json
@@ -5,5 +5,6 @@
},
"name":"Հայերեն",
"suggest-full-text-search": "որոնել '{{{SEARCH_TERMS}}}'..."
+ , "no-such-book": "Գիրքը բացակայում է՝ {{BOOK_NAME}}"
, "url-not-found" : "Սխալ հասցե՝ {{url}}"
}
diff --git a/static/i18n/qqq.json b/static/i18n/qqq.json
index f62e435e1..a0a3fbb1e 100644
--- a/static/i18n/qqq.json
+++ b/static/i18n/qqq.json
@@ -6,5 +6,6 @@
},
"name": "Current language to which the string is being translated to.",
"suggest-full-text-search": "Text appearing in the suggestion list that, when selected, runs a full text search instead of the title search"
+ , "no-such-book": "Error text when the requested book is not found in the library"
, "url-not-found" : "Error text about wrong URL for an HTTP 404 error"
}
diff --git a/test/server.cpp b/test/server.cpp
index 7508a7bf2..f89e2236a 100644
--- a/test/server.cpp
+++ b/test/server.cpp
@@ -571,6 +571,14 @@ TEST_F(ServerTest, 404WithBodyTesting)
)" },
+ { /* url */ "/ROOT/random?content=non-existent-book&userlang=hy",
+ expected_body==R"(
+ Not Found
+
+ Գիրքը բացակայում է՝ non-existent-book
+
+)" },
+
{ /* url */ "/ROOT/suggest?content=no-such-book&term=whatever",
expected_body==R"(
Not Found
From 1ace16229d4c10bb0c71ea1d851d0f6041cc08cd Mon Sep 17 00:00:00 2001
From: Veloman Yunkan
Date: Sun, 23 Jan 2022 17:23:06 +0400
Subject: [PATCH 11/28] Internationalized search suggestion message
---
src/server/internalServer.cpp | 17 +++++++++++------
static/i18n/en.json | 1 +
static/i18n/qqq.json | 1 +
3 files changed, 13 insertions(+), 6 deletions(-)
diff --git a/src/server/internalServer.cpp b/src/server/internalServer.cpp
index 60d8b0741..f7117c2e8 100644
--- a/src/server/internalServer.cpp
+++ b/src/server/internalServer.cpp
@@ -823,13 +823,18 @@ std::string get_book_name(const RequestContext& request)
}
}
+ParameterizedMessage suggestSearchMsg(const std::string& searchURL, const std::string& pattern)
+{
+ return ParameterizedMessage("suggest-search",
+ {
+ { "PATTERN", pattern },
+ { "SEARCH_URL", searchURL }
+ });
+}
+
std::string searchSuggestionHTML(const std::string& searchURL, const std::string& pattern)
{
- kainjow::mustache::mustache tmpl("Make a full text search for {{pattern}} ");
- MustacheData data;
- data.set("pattern", pattern);
- data.set("searchURL", searchURL);
- return (tmpl.render(data));
+ return suggestSearchMsg(searchURL, pattern).getText("en");
}
} // unnamed namespace
@@ -863,7 +868,7 @@ std::unique_ptr InternalServer::handle_content(const RequestContext& r
const std::string searchURL = m_root + "/search?pattern=" + kiwix::urlEncode(pattern, true);
return HTTP404HtmlResponse(*this, request)
+ urlNotFoundMsg
- + searchSuggestionHTML(searchURL, kiwix::urlDecode(pattern))
+ + suggestSearchMsg(searchURL, kiwix::urlDecode(pattern))
+ TaskbarInfo(bookName);
}
diff --git a/static/i18n/en.json b/static/i18n/en.json
index d6b197e76..32ce8fa78 100644
--- a/static/i18n/en.json
+++ b/static/i18n/en.json
@@ -7,4 +7,5 @@
"suggest-full-text-search": "containing '{{{SEARCH_TERMS}}}'..."
, "no-such-book": "No such book: {{BOOK_NAME}}"
, "url-not-found" : "The requested URL \"{{url}}\" was not found on this server."
+ , "suggest-search" : "Make a full text search for {{PATTERN}} "
}
diff --git a/static/i18n/qqq.json b/static/i18n/qqq.json
index a0a3fbb1e..8d1195b40 100644
--- a/static/i18n/qqq.json
+++ b/static/i18n/qqq.json
@@ -8,4 +8,5 @@
"suggest-full-text-search": "Text appearing in the suggestion list that, when selected, runs a full text search instead of the title search"
, "no-such-book": "Error text when the requested book is not found in the library"
, "url-not-found" : "Error text about wrong URL for an HTTP 404 error"
+ , "suggest-search" : "Suggest a search when the URL points to a non existing article"
}
From 52d4f73e89f2c4454746ef9d5b20f10116e7788f Mon Sep 17 00:00:00 2001
From: Veloman Yunkan
Date: Sun, 23 Jan 2022 21:50:14 +0400
Subject: [PATCH 12/28] RIP searchSuggestionHTML() & English-only message
---
src/server/internalServer.cpp | 7 +------
1 file changed, 1 insertion(+), 6 deletions(-)
diff --git a/src/server/internalServer.cpp b/src/server/internalServer.cpp
index f7117c2e8..7971840e6 100644
--- a/src/server/internalServer.cpp
+++ b/src/server/internalServer.cpp
@@ -832,11 +832,6 @@ ParameterizedMessage suggestSearchMsg(const std::string& searchURL, const std::s
});
}
-std::string searchSuggestionHTML(const std::string& searchURL, const std::string& pattern)
-{
- return suggestSearchMsg(searchURL, pattern).getText("en");
-}
-
} // unnamed namespace
std::unique_ptr
@@ -902,7 +897,7 @@ std::unique_ptr InternalServer::handle_content(const RequestContext& r
std::string searchURL = m_root + "/search?content=" + bookName + "&pattern=" + kiwix::urlEncode(pattern, true);
return HTTP404HtmlResponse(*this, request)
+ urlNotFoundMsg
- + searchSuggestionHTML(searchURL, kiwix::urlDecode(pattern))
+ + suggestSearchMsg(searchURL, kiwix::urlDecode(pattern))
+ TaskbarInfo(bookName, archive.get());
}
}
From ca7e0fb4a0bbf2989bda3d896202e87ef81fd83e Mon Sep 17 00:00:00 2001
From: Veloman Yunkan
Date: Sun, 23 Jan 2022 22:05:44 +0400
Subject: [PATCH 13/28] Internationalized random article failure message
---
src/server/internalServer.cpp | 8 ++++++--
static/i18n/en.json | 1 +
static/i18n/qqq.json | 1 +
3 files changed, 8 insertions(+), 2 deletions(-)
diff --git a/src/server/internalServer.cpp b/src/server/internalServer.cpp
index 7971840e6..876a5f7f6 100644
--- a/src/server/internalServer.cpp
+++ b/src/server/internalServer.cpp
@@ -462,6 +462,11 @@ std::string noSearchResultsMsg()
return "The fulltext search engine is not available for this content.";
}
+ParameterizedMessage nonParameterizedMessage(const std::string& msgId)
+{
+ return ParameterizedMessage(msgId, {});
+}
+
} // unnamed namespace
std::unique_ptr InternalServer::handle_suggest(const RequestContext& request)
@@ -680,9 +685,8 @@ std::unique_ptr InternalServer::handle_random(const RequestContext& re
auto entry = archive->getRandomEntry();
return build_redirect(bookName, getFinalItem(*archive, entry));
} catch(zim::EntryNotFound& e) {
- const std::string error_details = "Oops! Failed to pick a random article :(";
return HTTP404HtmlResponse(*this, request)
- + error_details
+ + nonParameterizedMessage("random-article-failure")
+ TaskbarInfo(bookName, archive.get());
}
}
diff --git a/static/i18n/en.json b/static/i18n/en.json
index 32ce8fa78..6c34ec729 100644
--- a/static/i18n/en.json
+++ b/static/i18n/en.json
@@ -8,4 +8,5 @@
, "no-such-book": "No such book: {{BOOK_NAME}}"
, "url-not-found" : "The requested URL \"{{url}}\" was not found on this server."
, "suggest-search" : "Make a full text search for {{PATTERN}} "
+ , "random-article-failure" : "Oops! Failed to pick a random article :("
}
diff --git a/static/i18n/qqq.json b/static/i18n/qqq.json
index 8d1195b40..cbf41ffaf 100644
--- a/static/i18n/qqq.json
+++ b/static/i18n/qqq.json
@@ -9,4 +9,5 @@
, "no-such-book": "Error text when the requested book is not found in the library"
, "url-not-found" : "Error text about wrong URL for an HTTP 404 error"
, "suggest-search" : "Suggest a search when the URL points to a non existing article"
+ , "random-article-failure" : "Failure of the random article selection procedure"
}
From 779382642b35de74896fd4548837abc1b637f545 Mon Sep 17 00:00:00 2001
From: Veloman Yunkan
Date: Sun, 23 Jan 2022 22:24:30 +0400
Subject: [PATCH 14/28] Internationalized bad raw access datatype message
---
src/server/internalServer.cpp | 8 ++++++--
static/i18n/en.json | 1 +
static/i18n/qqq.json | 1 +
3 files changed, 8 insertions(+), 2 deletions(-)
diff --git a/src/server/internalServer.cpp b/src/server/internalServer.cpp
index 876a5f7f6..f13b9e23a 100644
--- a/src/server/internalServer.cpp
+++ b/src/server/internalServer.cpp
@@ -462,6 +462,11 @@ std::string noSearchResultsMsg()
return "The fulltext search engine is not available for this content.";
}
+ParameterizedMessage invalidRawAccessMsg(const std::string& dt)
+{
+ return ParameterizedMessage("invalid-raw-data-type", { {"DATATYPE", dt} });
+}
+
ParameterizedMessage nonParameterizedMessage(const std::string& msgId)
{
return ParameterizedMessage(msgId, {});
@@ -924,10 +929,9 @@ std::unique_ptr InternalServer::handle_raw(const RequestContext& reque
}
if (kind != "meta" && kind!= "content") {
- const std::string error_details = kind + " is not a valid request for raw content.";
return HTTP404HtmlResponse(*this, request)
+ urlNotFoundMsg
- + error_details;
+ + invalidRawAccessMsg(kind);
}
std::shared_ptr archive;
diff --git a/static/i18n/en.json b/static/i18n/en.json
index 6c34ec729..4772302fe 100644
--- a/static/i18n/en.json
+++ b/static/i18n/en.json
@@ -9,4 +9,5 @@
, "url-not-found" : "The requested URL \"{{url}}\" was not found on this server."
, "suggest-search" : "Make a full text search for {{PATTERN}} "
, "random-article-failure" : "Oops! Failed to pick a random article :("
+ , "invalid-raw-data-type" : "{{DATATYPE}} is not a valid request for raw content."
}
diff --git a/static/i18n/qqq.json b/static/i18n/qqq.json
index cbf41ffaf..34f57dd01 100644
--- a/static/i18n/qqq.json
+++ b/static/i18n/qqq.json
@@ -10,4 +10,5 @@
, "url-not-found" : "Error text about wrong URL for an HTTP 404 error"
, "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//DATATYPE/...); allowed values are 'meta' and 'content'"
}
From d2c864b0109bb1a7fae16446b926daa2e852cbd5 Mon Sep 17 00:00:00 2001
From: Veloman Yunkan
Date: Sun, 23 Jan 2022 22:37:29 +0400
Subject: [PATCH 15/28] Internationalized raw-entry-not-found message
---
src/server/internalServer.cpp | 13 +++++++++++--
static/i18n/en.json | 1 +
static/i18n/qqq.json | 1 +
3 files changed, 13 insertions(+), 2 deletions(-)
diff --git a/src/server/internalServer.cpp b/src/server/internalServer.cpp
index f13b9e23a..7ee986935 100644
--- a/src/server/internalServer.cpp
+++ b/src/server/internalServer.cpp
@@ -467,6 +467,16 @@ ParameterizedMessage invalidRawAccessMsg(const std::string& dt)
return ParameterizedMessage("invalid-raw-data-type", { {"DATATYPE", dt} });
}
+ParameterizedMessage rawEntryNotFoundMsg(const std::string& dt, const std::string& entry)
+{
+ return ParameterizedMessage("raw-entry-not-found",
+ {
+ {"DATATYPE", dt},
+ {"ENTRY", entry},
+ }
+ );
+}
+
ParameterizedMessage nonParameterizedMessage(const std::string& msgId)
{
return ParameterizedMessage(msgId, {});
@@ -967,10 +977,9 @@ std::unique_ptr InternalServer::handle_raw(const RequestContext& reque
if (m_verbose.load()) {
printf("Failed to find %s\n", itemPath.c_str());
}
- const std::string error_details = "Cannot find " + kind + " entry " + itemPath;
return HTTP404HtmlResponse(*this, request)
+ urlNotFoundMsg
- + error_details;
+ + rawEntryNotFoundMsg(kind, itemPath);
}
}
diff --git a/static/i18n/en.json b/static/i18n/en.json
index 4772302fe..415d7fbd5 100644
--- a/static/i18n/en.json
+++ b/static/i18n/en.json
@@ -10,4 +10,5 @@
, "suggest-search" : "Make a full text search for {{PATTERN}} "
, "random-article-failure" : "Oops! Failed to pick a random article :("
, "invalid-raw-data-type" : "{{DATATYPE}} is not a valid request for raw content."
+ , "raw-entry-not-found" : "Cannot find {{DATATYPE}} entry {{ENTRY}}"
}
diff --git a/static/i18n/qqq.json b/static/i18n/qqq.json
index 34f57dd01..0ade75a60 100644
--- a/static/i18n/qqq.json
+++ b/static/i18n/qqq.json
@@ -11,4 +11,5 @@
, "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//DATATYPE/...); allowed values are 'meta' and 'content'"
+ , "raw-entry-not-found" : "Entry requested via the /raw endpoint was not found"
}
From fbd23a8329106b9dcaa113a2e588966c773642ef Mon Sep 17 00:00:00 2001
From: Veloman Yunkan
Date: Sun, 23 Jan 2022 23:24:09 +0400
Subject: [PATCH 16/28] Fully internationalized 400, 404 & 500 error pages
---
src/server/internalServer.cpp | 4 ++--
src/server/response.cpp | 25 +++++++++++++++----------
src/server/response.h | 5 +++--
static/i18n/en.json | 7 +++++++
static/i18n/hy.json | 2 ++
static/i18n/qqq.json | 7 +++++++
test/server.cpp | 9 ++++++---
7 files changed, 42 insertions(+), 17 deletions(-)
diff --git a/src/server/internalServer.cpp b/src/server/internalServer.cpp
index 7ee986935..f6026a79c 100644
--- a/src/server/internalServer.cpp
+++ b/src/server/internalServer.cpp
@@ -628,8 +628,8 @@ std::unique_ptr InternalServer::handle_search(const RequestContext& re
// 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)
return HTTPErrorHtmlResponse(*this, request, MHD_HTTP_NOT_FOUND,
- "Fulltext search unavailable",
- "Not Found",
+ "fulltext-search-unavailable",
+ "404-page-heading",
m_root + "/skin/search_results.css")
+ noSearchResultsMsg()
+ TaskbarInfo(searchInfo.bookName, archive.get());
diff --git a/src/server/response.cpp b/src/server/response.cpp
index 09a9b4745..ca157b868 100644
--- a/src/server/response.cpp
+++ b/src/server/response.cpp
@@ -87,6 +87,11 @@ std::unique_ptr Response::build_304(const InternalServer& server, cons
const UrlNotFoundMsg urlNotFoundMsg;
const InvalidUrlMsg invalidUrlMsg;
+std::string ContentResponseBlueprint::getMessage(const std::string& msgId) const
+{
+ return getTranslatedString(m_request.get_user_language(), msgId);
+}
+
std::unique_ptr ContentResponseBlueprint::generateResponseObject() const
{
auto r = ContentResponse::build(m_server, m_template, m_data, m_mimeType);
@@ -100,8 +105,8 @@ std::unique_ptr ContentResponseBlueprint::generateResponseObjec
HTTPErrorHtmlResponse::HTTPErrorHtmlResponse(const InternalServer& server,
const RequestContext& request,
int httpStatusCode,
- const std::string& pageTitleMsg,
- const std::string& headingMsg,
+ const std::string& pageTitleMsgId,
+ const std::string& headingMsgId,
const std::string& cssUrl)
: ContentResponseBlueprint(&server,
&request,
@@ -112,8 +117,8 @@ HTTPErrorHtmlResponse::HTTPErrorHtmlResponse(const InternalServer& server,
kainjow::mustache::list emptyList;
this->m_data = kainjow::mustache::object{
{"CSS_URL", onlyAsNonEmptyMustacheValue(cssUrl) },
- {"PAGE_TITLE", pageTitleMsg},
- {"PAGE_HEADING", headingMsg},
+ {"PAGE_TITLE", getMessage(pageTitleMsgId)},
+ {"PAGE_HEADING", getMessage(headingMsgId)},
{"details", emptyList}
};
}
@@ -123,8 +128,8 @@ HTTP404HtmlResponse::HTTP404HtmlResponse(const InternalServer& server,
: HTTPErrorHtmlResponse(server,
request,
MHD_HTTP_NOT_FOUND,
- "Content not found",
- "Not Found")
+ "404-page-title",
+ "404-page-heading")
{
}
@@ -151,8 +156,8 @@ HTTP400HtmlResponse::HTTP400HtmlResponse(const InternalServer& server,
: HTTPErrorHtmlResponse(server,
request,
MHD_HTTP_BAD_REQUEST,
- "Invalid request",
- "Invalid request")
+ "400-page-title",
+ "400-page-heading")
{
}
@@ -172,8 +177,8 @@ HTTP500HtmlResponse::HTTP500HtmlResponse(const InternalServer& server,
: HTTPErrorHtmlResponse(server,
request,
MHD_HTTP_INTERNAL_SERVER_ERROR,
- "Internal Server Error",
- "Internal Server Error")
+ "500-page-title",
+ "500-page-heading")
{
// operator+() is a state-modifying operator (akin to operator+=)
*this + "An internal server error occured. We are sorry about that :/";
diff --git a/src/server/response.h b/src/server/response.h
index d9a0ada88..9351cf6d4 100644
--- a/src/server/response.h
+++ b/src/server/response.h
@@ -167,6 +167,7 @@ public: // functions
ContentResponseBlueprint& operator+(const TaskbarInfo& taskbarInfo);
protected: // functions
+ std::string getMessage(const std::string& msgId) const;
virtual std::unique_ptr generateResponseObject() const;
public: //data
@@ -184,8 +185,8 @@ struct HTTPErrorHtmlResponse : ContentResponseBlueprint
HTTPErrorHtmlResponse(const InternalServer& server,
const RequestContext& request,
int httpStatusCode,
- const std::string& pageTitleMsg,
- const std::string& headingMsg,
+ const std::string& pageTitleMsgId,
+ const std::string& headingMsgId,
const std::string& cssUrl = "");
using ContentResponseBlueprint::operator+;
diff --git a/static/i18n/en.json b/static/i18n/en.json
index 415d7fbd5..a6e62d51f 100644
--- a/static/i18n/en.json
+++ b/static/i18n/en.json
@@ -11,4 +11,11 @@
, "random-article-failure" : "Oops! Failed to pick a random article :("
, "invalid-raw-data-type" : "{{DATATYPE}} is not a valid request for raw content."
, "raw-entry-not-found" : "Cannot find {{DATATYPE}} entry {{ENTRY}}"
+ , "400-page-title" : "Invalid request"
+ , "400-page-heading" : "Invalid request"
+ , "404-page-title" : "Content not found"
+ , "404-page-heading" : "Not Found"
+ , "500-page-title" : "Internal Server Error"
+ , "500-page-heading" : "Internal Server Error"
+ , "fulltext-search-unavailable" : "Fulltext search unavailable"
}
diff --git a/static/i18n/hy.json b/static/i18n/hy.json
index 9b55d8c38..6120c2f0c 100644
--- a/static/i18n/hy.json
+++ b/static/i18n/hy.json
@@ -7,4 +7,6 @@
"suggest-full-text-search": "որոնել '{{{SEARCH_TERMS}}}'..."
, "no-such-book": "Գիրքը բացակայում է՝ {{BOOK_NAME}}"
, "url-not-found" : "Սխալ հասցե՝ {{url}}"
+ , "404-page-title" : "Սխալ հասցե"
+ , "404-page-heading" : "Սխալ հասցե"
}
diff --git a/static/i18n/qqq.json b/static/i18n/qqq.json
index 0ade75a60..b548495a9 100644
--- a/static/i18n/qqq.json
+++ b/static/i18n/qqq.json
@@ -12,4 +12,11 @@
, "random-article-failure" : "Failure of the random article selection procedure"
, "invalid-raw-data-type" : "Invalid DATATYPE was used with the /raw endpoint (/raw//DATATYPE/...); allowed values are 'meta' and 'content'"
, "raw-entry-not-found" : "Entry requested via the /raw endpoint was not found"
+ , "400-page-title" : "Title of the 400 error page"
+ , "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"
+ , "500-page-title" : "Title of the 500 error page"
+ , "500-page-heading" : "Heading of the 500 error page"
+ , "fulltext-search-unavailable" : "Title of the error page returned when search is attempted in a book without fulltext search database"
}
diff --git a/test/server.cpp b/test/server.cpp
index f89e2236a..65c1376a8 100644
--- a/test/server.cpp
+++ b/test/server.cpp
@@ -572,8 +572,9 @@ TEST_F(ServerTest, 404WithBodyTesting)
)" },
{ /* url */ "/ROOT/random?content=non-existent-book&userlang=hy",
+ expected_page_title=="Սխալ հասցե" &&
expected_body==R"(
- Not Found
+ Սխալ հասցե
Գիրքը բացակայում է՝ non-existent-book
@@ -596,8 +597,9 @@ TEST_F(ServerTest, 404WithBodyTesting)
)" },
{ /* url */ "/ROOT/catalog/?userlang=hy",
+ expected_page_title=="Սխալ հասցե" &&
expected_body==R"(
- Not Found
+ Սխալ հասցե
Սխալ հասցե՝ /ROOT/catalog/
@@ -612,8 +614,9 @@ TEST_F(ServerTest, 404WithBodyTesting)
)" },
{ /* url */ "/ROOT/catalog/invalid_endpoint?userlang=hy",
+ expected_page_title=="Սխալ հասցե" &&
expected_body==R"(
- Not Found
+ Սխալ հասցե
Սխալ հասցե՝ /ROOT/catalog/invalid_endpoint
From 6f3db20078f1e6f7a28780b52d324164ae3245d7 Mon Sep 17 00:00:00 2001
From: Veloman Yunkan
Date: Wed, 6 Apr 2022 14:25:36 +0400
Subject: [PATCH 17/28] Internationalized "Fulltext search unavailable" page
---
src/server/internalServer.cpp | 7 +------
static/i18n/en.json | 1 +
static/i18n/qqq.json | 1 +
3 files changed, 3 insertions(+), 6 deletions(-)
diff --git a/src/server/internalServer.cpp b/src/server/internalServer.cpp
index f6026a79c..d34c5d69c 100644
--- a/src/server/internalServer.cpp
+++ b/src/server/internalServer.cpp
@@ -457,11 +457,6 @@ ParameterizedMessage noSuchBookErrorMsg(const std::string& bookName)
return ParameterizedMessage("no-such-book", { {"BOOK_NAME", bookName} });
}
-std::string noSearchResultsMsg()
-{
- return "The fulltext search engine is not available for this content.";
-}
-
ParameterizedMessage invalidRawAccessMsg(const std::string& dt)
{
return ParameterizedMessage("invalid-raw-data-type", { {"DATATYPE", dt} });
@@ -631,7 +626,7 @@ std::unique_ptr InternalServer::handle_search(const RequestContext& re
"fulltext-search-unavailable",
"404-page-heading",
m_root + "/skin/search_results.css")
- + noSearchResultsMsg()
+ + nonParameterizedMessage("no-search-results")
+ TaskbarInfo(searchInfo.bookName, archive.get());
}
diff --git a/static/i18n/en.json b/static/i18n/en.json
index a6e62d51f..4b4f4f748 100644
--- a/static/i18n/en.json
+++ b/static/i18n/en.json
@@ -18,4 +18,5 @@
, "500-page-title" : "Internal Server Error"
, "500-page-heading" : "Internal Server Error"
, "fulltext-search-unavailable" : "Fulltext search unavailable"
+ , "no-search-results": "The fulltext search engine is not available for this content."
}
diff --git a/static/i18n/qqq.json b/static/i18n/qqq.json
index b548495a9..ff467cbe0 100644
--- a/static/i18n/qqq.json
+++ b/static/i18n/qqq.json
@@ -19,4 +19,5 @@
, "500-page-title" : "Title of the 500 error page"
, "500-page-heading" : "Heading of the 500 error page"
, "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"
}
From 901664b097c61659fec0ecbe1aa079adbcbcb0b5 Mon Sep 17 00:00:00 2001
From: Veloman Yunkan
Date: Sun, 30 Jan 2022 20:00:02 +0400
Subject: [PATCH 18/28] "Go to welcome page" in taskbar isn't translated
The (failing) tests now demonstrate that some text in the taskbar is not
translated. Will fix in the next commit.
---
test/server.cpp | 32 +++++++++++++++++++++++++++-----
1 file changed, 27 insertions(+), 5 deletions(-)
diff --git a/test/server.cpp b/test/server.cpp
index 65c1376a8..e82dd676e 100644
--- a/test/server.cpp
+++ b/test/server.cpp
@@ -411,11 +411,13 @@ public:
std::string expectedResponse() const;
private:
+ bool isTranslatedVersion() const;
virtual std::string pageTitle() const;
std::string pageCssLink() const;
std::string hiddenBookNameInput() const;
std::string searchPatternInput() const;
std::string taskbarLinks() const;
+ std::string goToWelcomePageText() const;
};
std::string TestContentIn404HtmlResponse::expectedResponse() const
@@ -454,7 +456,11 @@ std::string TestContentIn404HtmlResponse::expectedResponse() const
diff --git a/test/server.cpp b/test/server.cpp
index 79095863d..b724ca41c 100644
--- a/test/server.cpp
+++ b/test/server.cpp
@@ -520,11 +520,15 @@ std::string TestContentIn404HtmlResponse::hiddenBookNameInput() const
std::string TestContentIn404HtmlResponse::searchPatternInput() const
{
- return R"(
+ const std::string searchboxTooltip = isTranslatedVersion()
+ ? "Որոնել '" + bookTitle + "'֊ում"
+ : "Search '" + bookTitle + "'";
+
+ return R"(
)";
}
From 9987fbd488c7ad78e4c9cceed7504eda31abdb8f Mon Sep 17 00:00:00 2001
From: Veloman Yunkan
Date: Thu, 7 Apr 2022 20:19:05 +0400
Subject: [PATCH 27/28] Fixed CI build failure under android_arm*
---
src/server/internalServer.cpp | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/server/internalServer.cpp b/src/server/internalServer.cpp
index d34c5d69c..a60de9e9f 100644
--- a/src/server/internalServer.cpp
+++ b/src/server/internalServer.cpp
@@ -474,7 +474,8 @@ ParameterizedMessage rawEntryNotFoundMsg(const std::string& dt, const std::strin
ParameterizedMessage nonParameterizedMessage(const std::string& msgId)
{
- return ParameterizedMessage(msgId, {});
+ const ParameterizedMessage::Parameters noParams;
+ return ParameterizedMessage(msgId, noParams);
}
} // unnamed namespace
From 927c12574a99559d06e30e9a9553ecfdc4f6f422 Mon Sep 17 00:00:00 2001
From: Veloman Yunkan
Date: Sat, 9 Apr 2022 13:32:34 +0400
Subject: [PATCH 28/28] Preliminary support for Accept-Language: header
In the absence of the "userlang" query parameter in the URL, the value
of the "Accept-Language" header is used. However, it is assumed that
"Accept-Language" specifies a single language (rather than a comma
separated list of languages possibly weighted with quality values).
Example:
Accept-Language: fr
// should work
Accept-Language: fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5
// The requested language will be considered to be
// "fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5".
// The i18n code will fail to find resources for such a language
// and will use the default "en" instead.
---
src/server/request_context.cpp | 10 ++++-
test/server.cpp | 74 +++++++++++++++++++++++++++++++++-
2 files changed, 82 insertions(+), 2 deletions(-)
diff --git a/src/server/request_context.cpp b/src/server/request_context.cpp
index 5ac9fe8dd..0946b123b 100644
--- a/src/server/request_context.cpp
+++ b/src/server/request_context.cpp
@@ -195,7 +195,15 @@ std::string RequestContext::get_query() const {
std::string RequestContext::get_user_language() const
{
- return get_optional_param("userlang", "en");
+ try {
+ return get_argument("userlang");
+ } catch(const std::out_of_range&) {}
+
+ try {
+ return get_header("Accept-Language");
+ } catch(const std::out_of_range&) {}
+
+ return "en";
}
}
diff --git a/test/server.cpp b/test/server.cpp
index b724ca41c..2fa31f203 100644
--- a/test/server.cpp
+++ b/test/server.cpp
@@ -57,7 +57,6 @@ std::string removeEOLWhitespaceMarkers(const std::string& s)
return std::regex_replace(s, pattern, "");
}
-
class ZimFileServer
{
public: // types
@@ -879,6 +878,79 @@ TEST_F(ServerTest, 500)
EXPECT_EQ(r->body, expectedBody);
}
+TEST_F(ServerTest, UserLanguageControl)
+{
+ struct TestData
+ {
+ const std::string url;
+ const std::string acceptLanguageHeader;
+ const std::string expectedH1;
+
+ operator TestContext() const
+ {
+ return TestContext{
+ {"url", url},
+ {"acceptLanguageHeader", acceptLanguageHeader},
+ };
+ }
+ };
+
+ const TestData testData[] = {
+ {
+ /*url*/ "/ROOT/zimfile/invalid-article",
+ /*Accept-Language:*/ "",
+ /* expected */ "Not Found"
+ },
+ {
+ /*url*/ "/ROOT/zimfile/invalid-article?userlang=en",
+ /*Accept-Language:*/ "",
+ /* expected */ "Not Found"
+ },
+ {
+ /*url*/ "/ROOT/zimfile/invalid-article?userlang=hy",
+ /*Accept-Language:*/ "",
+ /* expected */ "Սխալ հասցե"
+ },
+ {
+ /*url*/ "/ROOT/zimfile/invalid-article",
+ /*Accept-Language:*/ "*",
+ /* expected */ "Not Found"
+ },
+ {
+ /*url*/ "/ROOT/zimfile/invalid-article",
+ /*Accept-Language:*/ "hy",
+ /* expected */ "Սխալ հասցե"
+ },
+ {
+ // userlang query parameter takes precedence over Accept-Language
+ /*url*/ "/ROOT/zimfile/invalid-article?userlang=en",
+ /*Accept-Language:*/ "hy",
+ /* expected */ "Not Found"
+ },
+ {
+ // The value of the Accept-Language header is not currently parsed.
+ // In case of a comma separated list of languages (optionally weighted
+ // with quality values) the default (en) language is used instead.
+ /*url*/ "/ROOT/zimfile/invalid-article",
+ /*Accept-Language:*/ "hy;q=0.9, en;q=0.2",
+ /* expected */ "Not Found"
+ },
+ };
+
+ const std::regex h1Regex("(.+) ");
+ for ( const auto& t : testData ) {
+ std::smatch h1Match;
+ Headers headers;
+ if ( !t.acceptLanguageHeader.empty() ) {
+ headers.insert({"Accept-Language", t.acceptLanguageHeader});
+ }
+ const auto r = zfs1_->GET(t.url.c_str(), headers);
+ std::regex_search(r->body, h1Match, h1Regex);
+ const std::string h1(h1Match[1]);
+ EXPECT_EQ(h1, t.expectedH1) << t;
+ }
+}
+
TEST_F(ServerTest, RandomPageRedirectsToAnExistingArticle)
{
auto g = zfs1_->GET("/ROOT/random?content=zimfile");