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 +namespace i18n { +// this data is generated by the i18n resource compiler +extern const I18nStringTable stringTables[]; +extern const size_t langCount; +} -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 } -}; +namespace +{ 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 @@ class ContentResponseBlueprint 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 @@ class TestContentIn404HtmlResponse : public ExpectedResponseData 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
- + )FRAG", R"FRAG( @@ -478,10 +484,14 @@ std::string TestContentIn404HtmlResponse::expectedResponse() const + frag[3] + searchPatternInput() + frag[4] - + taskbarLinks() + + goToWelcomePageText() + frag[5] - + removeEOLWhitespaceMarkers(expectedBody) - + frag[6]; + + goToWelcomePageText() + + frag[6] + + taskbarLinks() + + frag[7] + + expectedBody + + frag[8]; } std::string TestContentIn404HtmlResponse::pageTitle() const @@ -538,6 +548,19 @@ std::string TestContentIn404HtmlResponse::taskbarLinks() const + R"(">)"; } +bool TestContentIn404HtmlResponse::isTranslatedVersion() const +{ + return url.find("userlang=hy") != std::string::npos; +} + +std::string TestContentIn404HtmlResponse::goToWelcomePageText() const +{ + return isTranslatedVersion() + ? "Գրադարանի էջ" + : "Go to welcome page"; +} + + class TestContentIn400HtmlResponse : public TestContentIn404HtmlResponse { public: @@ -556,7 +579,6 @@ std::string TestContentIn400HtmlResponse::pageTitle() const { : expectedPageTitle; } - } // namespace TestingOfHtmlResponses TEST_F(ServerTest, 404WithBodyTesting) From c2bfeb4030edbae35cadbe8653bb6fb41012abb8 Mon Sep 17 00:00:00 2001 From: Veloman Yunkan Date: Sun, 30 Jan 2022 20:55:19 +0400 Subject: [PATCH 19/28] "Go to welcome page" is internationalized --- src/server/i18n.h | 14 ++++++++++++++ src/server/response.cpp | 6 ++++-- src/server/response.h | 2 +- static/i18n/en.json | 1 + static/i18n/hy.json | 1 + static/i18n/qqq.json | 1 + static/templates/taskbar_part.html | 2 +- 7 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/server/i18n.h b/src/server/i18n.h index 955f13091..3a3698f9c 100644 --- a/src/server/i18n.h +++ b/src/server/i18n.h @@ -50,6 +50,20 @@ 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); + } + +private: + const std::string m_lang; +}; + } // namespace i18n struct ParameterizedMessage diff --git a/src/server/response.cpp b/src/server/response.cpp index ca157b868..56675c75b 100644 --- a/src/server/response.cpp +++ b/src/server/response.cpp @@ -280,14 +280,16 @@ void print_response_info(int retCode, MHD_Response* response) } -void ContentResponse::introduce_taskbar() +void ContentResponse::introduce_taskbar(const std::string& lang) { + i18n::GetTranslatedString t(lang); kainjow::mustache::data data; data.set("root", m_root); data.set("content", m_bookName); data.set("hascontent", (!m_bookName.empty() && !m_bookTitle.empty())); data.set("title", m_bookTitle); data.set("withlibrarybutton", m_withLibraryButton); + data.set("LIBRARY_BUTTON_TEXT", t("library-button-text")); auto head_content = render_template(RESOURCE::templates::head_taskbar_html, data); m_content = prependToFirstOccurence( m_content, @@ -352,7 +354,7 @@ ContentResponse::create_mhd_response(const RequestContext& request) inject_root_link(); if (m_withTaskbar) { - introduce_taskbar(); + introduce_taskbar(request.get_user_language()); } if (m_blockExternalLinks) { inject_externallinks_blocker(); diff --git a/src/server/response.h b/src/server/response.h index 9351cf6d4..2eba46d2b 100644 --- a/src/server/response.h +++ b/src/server/response.h @@ -106,7 +106,7 @@ class ContentResponse : public Response { private: MHD_Response* create_mhd_response(const RequestContext& request); - void introduce_taskbar(); + void introduce_taskbar(const std::string& lang); void inject_externallinks_blocker(); void inject_root_link(); bool can_compress(const RequestContext& request) const; diff --git a/static/i18n/en.json b/static/i18n/en.json index 4b4f4f748..2f24a1a4b 100644 --- a/static/i18n/en.json +++ b/static/i18n/en.json @@ -19,4 +19,5 @@ , "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." + , "library-button-text": "Go to welcome page" } diff --git a/static/i18n/hy.json b/static/i18n/hy.json index 6120c2f0c..6fd473363 100644 --- a/static/i18n/hy.json +++ b/static/i18n/hy.json @@ -9,4 +9,5 @@ , "url-not-found" : "Սխալ հասցե՝ {{url}}" , "404-page-title" : "Սխալ հասցե" , "404-page-heading" : "Սխալ հասցե" + , "library-button-text": "Գրադարանի էջ" } diff --git a/static/i18n/qqq.json b/static/i18n/qqq.json index ff467cbe0..20860ed5a 100644 --- a/static/i18n/qqq.json +++ b/static/i18n/qqq.json @@ -20,4 +20,5 @@ , "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" + , "library-button-text": "Tooltip of the button leading to the welcome page" } diff --git a/static/templates/taskbar_part.html b/static/templates/taskbar_part.html index 4ad99ad04..b3268769a 100644 --- a/static/templates/taskbar_part.html +++ b/static/templates/taskbar_part.html @@ -12,7 +12,7 @@
{{#withlibrarybutton}} - + {{/withlibrarybutton}} {{#hascontent}} From f73be3cde7223302568709e1d956e8206a691815 Mon Sep 17 00:00:00 2001 From: Veloman Yunkan Date: Sun, 30 Jan 2022 21:19:47 +0400 Subject: [PATCH 20/28] Initializing mustache data via initializer list --- src/server/response.cpp | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/server/response.cpp b/src/server/response.cpp index 56675c75b..bc2337add 100644 --- a/src/server/response.cpp +++ b/src/server/response.cpp @@ -283,13 +283,14 @@ void print_response_info(int retCode, MHD_Response* response) void ContentResponse::introduce_taskbar(const std::string& lang) { i18n::GetTranslatedString t(lang); - kainjow::mustache::data data; - data.set("root", m_root); - data.set("content", m_bookName); - data.set("hascontent", (!m_bookName.empty() && !m_bookTitle.empty())); - data.set("title", m_bookTitle); - data.set("withlibrarybutton", m_withLibraryButton); - data.set("LIBRARY_BUTTON_TEXT", t("library-button-text")); + kainjow::mustache::object data{ + {"root", m_root}, + {"content", m_bookName}, + {"hascontent", (!m_bookName.empty() && !m_bookTitle.empty())}, + {"title", m_bookTitle}, + {"withlibrarybutton", m_withLibraryButton}, + {"LIBRARY_BUTTON_TEXT", t("library-button-text")}, + }; auto head_content = render_template(RESOURCE::templates::head_taskbar_html, data); m_content = prependToFirstOccurence( m_content, From ed7717c1e70fc762f086facde758a02bc55244e5 Mon Sep 17 00:00:00 2001 From: Veloman Yunkan Date: Sun, 30 Jan 2022 21:28:06 +0400 Subject: [PATCH 21/28] Testing the translation of "Go to the main page" The new test fails since the "Go to the main page" button is not yet internationalized. --- test/server.cpp | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/test/server.cpp b/test/server.cpp index e82dd676e..ca44875d8 100644 --- a/test/server.cpp +++ b/test/server.cpp @@ -533,11 +533,15 @@ std::string TestContentIn404HtmlResponse::taskbarLinks() const if ( bookName.empty() ) return ""; - return R"( {{/withlibrarybutton}} {{#hascontent}} - + {{/hascontent}} From 527a606281f382f3d22bee3afd078e973752700c Mon Sep 17 00:00:00 2001 From: Veloman Yunkan Date: Sun, 30 Jan 2022 21:44:44 +0400 Subject: [PATCH 23/28] Testing the translation of "Go to random page" The new test fails since the "Go to random page" button is not yet internationalized. --- test/server.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/test/server.cpp b/test/server.cpp index ca44875d8..de0d78e78 100644 --- a/test/server.cpp +++ b/test/server.cpp @@ -537,6 +537,10 @@ std::string TestContentIn404HtmlResponse::taskbarLinks() const ? "Դեպի '" + bookTitle + "'֊ի գլխավոր էջը" : "Go to the main page of '" + bookTitle + "'"; + const std::string goToRandomPage = isTranslatedVersion() + ? "Բացել պատահական էջ" + : "Go to a randomly selected page"; + return R"( - )"; From 11be821c46dc50bdc9fb794eef84eecb2bc40234 Mon Sep 17 00:00:00 2001 From: Veloman Yunkan Date: Sun, 30 Jan 2022 21:59:49 +0400 Subject: [PATCH 24/28] Internationalized "Go to a randomly selected page" At this point a potential issue has been revealed. Now we produce the final HTML via 2-level template expansion 1. Render parameterized messages 2. Render the HTML template In which templates we should use double mustache "{{}}" (HTML-escaping) tags and where we may use triple mustache "{{{}}}" (non-escaping) tags? --- src/server/response.cpp | 1 + static/i18n/en.json | 1 + static/i18n/hy.json | 1 + static/i18n/qqq.json | 1 + static/templates/taskbar_part.html | 2 +- 5 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/server/response.cpp b/src/server/response.cpp index fa1365cf0..7046a289e 100644 --- a/src/server/response.cpp +++ b/src/server/response.cpp @@ -291,6 +291,7 @@ void ContentResponse::introduce_taskbar(const std::string& lang) {"withlibrarybutton", m_withLibraryButton}, {"LIBRARY_BUTTON_TEXT", t("library-button-text")}, {"HOME_BUTTON_TEXT", t("home-button-text", {{"BOOK_TITLE", m_bookTitle}}) }, + {"RANDOM_PAGE_BUTTON_TEXT", t("random-page-button-text") }, }; auto head_content = render_template(RESOURCE::templates::head_taskbar_html, data); m_content = prependToFirstOccurence( diff --git a/static/i18n/en.json b/static/i18n/en.json index 8b9906db5..ca00d9691 100644 --- a/static/i18n/en.json +++ b/static/i18n/en.json @@ -21,4 +21,5 @@ , "no-search-results": "The fulltext search engine is not available for this content." , "library-button-text": "Go to welcome page" , "home-button-text": "Go to the main page of '{{BOOK_TITLE}}'" + , "random-page-button-text": "Go to a randomly selected page" } diff --git a/static/i18n/hy.json b/static/i18n/hy.json index 8892761ac..b64c2fd7b 100644 --- a/static/i18n/hy.json +++ b/static/i18n/hy.json @@ -11,4 +11,5 @@ , "404-page-heading" : "Սխալ հասցե" , "library-button-text": "Գրադարանի էջ" , "home-button-text": "Դեպի '{{BOOK_TITLE}}'֊ի գլխավոր էջը" + , "random-page-button-text": "Բացել պատահական էջ" } diff --git a/static/i18n/qqq.json b/static/i18n/qqq.json index 436d666cd..c705fecb3 100644 --- a/static/i18n/qqq.json +++ b/static/i18n/qqq.json @@ -22,4 +22,5 @@ , "no-search-results": "Text of the error page returned when search is attempted in a book without fulltext search database" , "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" } diff --git a/static/templates/taskbar_part.html b/static/templates/taskbar_part.html index 3992be3c4..b76c1c0d4 100644 --- a/static/templates/taskbar_part.html +++ b/static/templates/taskbar_part.html @@ -16,7 +16,7 @@ {{/withlibrarybutton}} {{#hascontent}} - {{/hascontent}}
From 5052d4018ce43f50e8e0d2024755fe58483599ff Mon Sep 17 00:00:00 2001 From: Veloman Yunkan Date: Sun, 30 Jan 2022 22:11:11 +0400 Subject: [PATCH 25/28] hy translation of the suggest-search message --- static/i18n/hy.json | 1 + test/server.cpp | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/static/i18n/hy.json b/static/i18n/hy.json index b64c2fd7b..df9ad481b 100644 --- a/static/i18n/hy.json +++ b/static/i18n/hy.json @@ -7,6 +7,7 @@ "suggest-full-text-search": "որոնել '{{{SEARCH_TERMS}}}'..." , "no-such-book": "Գիրքը բացակայում է՝ {{BOOK_NAME}}" , "url-not-found" : "Սխալ հասցե՝ {{url}}" + , "suggest-search" : "Որոնել {{PATTERN}}" , "404-page-title" : "Սխալ հասցե" , "404-page-heading" : "Սխալ հասցե" , "library-button-text": "Գրադարանի էջ" diff --git a/test/server.cpp b/test/server.cpp index de0d78e78..79095863d 100644 --- a/test/server.cpp +++ b/test/server.cpp @@ -714,7 +714,7 @@ TEST_F(ServerTest, 404WithBodyTesting) Սխալ հասցե՝ /ROOT/zimfile/invalid-article

- Make a full text search for invalid-article + Որոնել invalid-article

)" }, From a0d9a824e13d25ef37d9cdb6a3538e4164fc812b Mon Sep 17 00:00:00 2001 From: Veloman Yunkan Date: Sun, 30 Jan 2022 22:22:17 +0400 Subject: [PATCH 26/28] Internationalized searchbox tooltip --- src/server/response.cpp | 1 + static/i18n/en.json | 1 + static/i18n/hy.json | 1 + static/i18n/qqq.json | 1 + static/templates/taskbar_part.html | 2 +- test/server.cpp | 14 +++++++++----- 6 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/server/response.cpp b/src/server/response.cpp index 7046a289e..20a236020 100644 --- a/src/server/response.cpp +++ b/src/server/response.cpp @@ -292,6 +292,7 @@ void ContentResponse::introduce_taskbar(const std::string& lang) {"LIBRARY_BUTTON_TEXT", t("library-button-text")}, {"HOME_BUTTON_TEXT", t("home-button-text", {{"BOOK_TITLE", m_bookTitle}}) }, {"RANDOM_PAGE_BUTTON_TEXT", t("random-page-button-text") }, + {"SEARCHBOX_TOOLTIP", t("searchbox-tooltip", {{"BOOK_TITLE", m_bookTitle}}) }, }; auto head_content = render_template(RESOURCE::templates::head_taskbar_html, data); m_content = prependToFirstOccurence( diff --git a/static/i18n/en.json b/static/i18n/en.json index ca00d9691..183f8b9f4 100644 --- a/static/i18n/en.json +++ b/static/i18n/en.json @@ -22,4 +22,5 @@ , "library-button-text": "Go to welcome page" , "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}}'" } diff --git a/static/i18n/hy.json b/static/i18n/hy.json index df9ad481b..d4a6ff56b 100644 --- a/static/i18n/hy.json +++ b/static/i18n/hy.json @@ -13,4 +13,5 @@ , "library-button-text": "Գրադարանի էջ" , "home-button-text": "Դեպի '{{BOOK_TITLE}}'֊ի գլխավոր էջը" , "random-page-button-text": "Բացել պատահական էջ" + , "searchbox-tooltip": "Որոնել '{{BOOK_TITLE}}'֊ում" } diff --git a/static/i18n/qqq.json b/static/i18n/qqq.json index c705fecb3..6adb66e18 100644 --- a/static/i18n/qqq.json +++ b/static/i18n/qqq.json @@ -23,4 +23,5 @@ , "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" + , "searchbox-tooltip": "Tooltip displayed for the search box" } diff --git a/static/templates/taskbar_part.html b/static/templates/taskbar_part.html index b76c1c0d4..42c76ed4b 100644 --- a/static/templates/taskbar_part.html +++ b/static/templates/taskbar_part.html @@ -5,7 +5,7 @@
{{#hascontent}}{{/hascontent}} - +
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");