Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Internationalization of kiwix-serve's backend #679

Merged
merged 28 commits into from
Apr 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
a18dd82
Introduced makeFulltextSearchSuggestion() helper
veloman-yunkan Mar 24, 2022
c574735
makeFulltextSearchSuggestion() works via mustache
veloman-yunkan Jan 16, 2022
d029c2b
Enter I18nStringDB
veloman-yunkan Jan 16, 2022
507e111
i18n data is kept in and generated from JSON files
veloman-yunkan Jan 16, 2022
e4a0a02
User language control via userlang query param
veloman-yunkan Jan 16, 2022
577b6e2
kiwix::i18n::expandParameterizedString()
veloman-yunkan Jan 20, 2022
202ec81
URL-not-found message went into i18n JSON resource
veloman-yunkan Mar 24, 2022
387f977
Enter ParameterizedMessage
veloman-yunkan Mar 29, 2022
b2526c7
Translation of the url-not-found message
veloman-yunkan Mar 24, 2022
cb5ae01
Localized "No such book" 404 message for /random
veloman-yunkan Mar 24, 2022
1ace162
Internationalized search suggestion message
veloman-yunkan Jan 23, 2022
52d4f73
RIP searchSuggestionHTML() & English-only message
veloman-yunkan Jan 23, 2022
ca7e0fb
Internationalized random article failure message
veloman-yunkan Jan 23, 2022
7793826
Internationalized bad raw access datatype message
veloman-yunkan Jan 23, 2022
d2c864b
Internationalized raw-entry-not-found message
veloman-yunkan Jan 23, 2022
fbd23a8
Fully internationalized 400, 404 & 500 error pages
veloman-yunkan Jan 23, 2022
6f3db20
Internationalized "Fulltext search unavailable" page
veloman-yunkan Apr 6, 2022
901664b
"Go to welcome page" in taskbar isn't translated
veloman-yunkan Jan 30, 2022
c2bfeb4
"Go to welcome page" is internationalized
veloman-yunkan Jan 30, 2022
f73be3c
Initializing mustache data via initializer list
veloman-yunkan Jan 30, 2022
ed7717c
Testing the translation of "Go to the main page"
veloman-yunkan Jan 30, 2022
3da81a3
Internationalized "Go to the main page" button
veloman-yunkan Jan 30, 2022
527a606
Testing the translation of "Go to random page"
veloman-yunkan Jan 30, 2022
11be821
Internationalized "Go to a randomly selected page"
veloman-yunkan Jan 30, 2022
5052d40
hy translation of the suggest-search message
veloman-yunkan Jan 30, 2022
a0d9a82
Internationalized searchbox tooltip
veloman-yunkan Jan 30, 2022
9987fbd
Fixed CI build failure under android_arm*
veloman-yunkan Apr 7, 2022
927c125
Preliminary support for Accept-Language: header
veloman-yunkan Apr 9, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions debian/libkiwix-dev.manpages
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
usr/share/man/man1/kiwix-compile-resources.1*
usr/share/man/man1/kiwix-compile-i18n.1*
161 changes: 161 additions & 0 deletions scripts/kiwix-compile-i18n
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
#!/usr/bin/env python3

'''
Copyright 2022 Veloman Yunkan <veloman.yunkan@gmail.com>

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or any
later version.

This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
02110-1301, USA.
'''

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))

18 changes: 18 additions & 0 deletions scripts/kiwix-compile-i18n.1
Original file line number Diff line number Diff line change
@@ -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 <veloman.yunkan@gmail.com>
2 changes: 1 addition & 1 deletion scripts/kiwix-compile-resources
Original file line number Diff line number Diff line change
Expand Up @@ -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 <stdlib.h>
#include <fstream>
Expand Down
6 changes: 6 additions & 0 deletions scripts/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -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')
2 changes: 2 additions & 0 deletions src/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,12 @@ kiwix_sources = [
'server/response.cpp',
'server/internalServer.cpp',
'server/internalServer_catalog_v2.cpp',
'server/i18n.cpp',
'opds_catalog.cpp',
'version.cpp'
]
kiwix_sources += lib_resources
kiwix_sources += i18n_resources

if host_machine.system() == 'windows'
kiwix_sources += 'subprocess_windows.cpp'
Expand Down
114 changes: 114 additions & 0 deletions src/server/i18n.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
* Copyright 2022 Veloman Yunkan <veloman.yunkan@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/

#include "i18n.h"

#include "tools/otherTools.h"

#include <algorithm>
#include <map>

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 i18n
{
// this data is generated by the i18n resource compiler
extern const I18nStringTable stringTables[];
extern const size_t langCount;
}

namespace
{

class I18nStringDB
{
public: // functions
I18nStringDB() {
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");
};

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<std::string, const I18nStringTable*> 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 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

std::string ParameterizedMessage::getText(const std::string& lang) const
{
return i18n::expandParameterizedString(lang, msgId, params);
}

} // namespace kiwix
Loading