From 05541ed49b8ad5a9988b55c1658e2221c37f5a7a Mon Sep 17 00:00:00 2001
From: HarpyWar <harpywar@gmail.com>
Date: Sun, 22 Jun 2014 20:37:06 +0400
Subject: [PATCH] add base for localization
 https://github.com/HarpyWar/pvpgn/issues/13

---
 src/bnetd/CMakeLists.txt   |   1 +
 src/bnetd/command.cpp      |   4 +-
 src/bnetd/i18n.cpp         | 236 +++++++++++++++++++++++++++++++++++++
 src/bnetd/i18n.h           |  58 +++++++++
 src/bnetd/luainterface.cpp |   1 +
 src/bnetd/main.cpp         |   4 +-
 src/bnetd/prefs.cpp        |  27 +++++
 src/bnetd/prefs.h          |   1 +
 src/bnetd/server.cpp       |   6 +
 src/bnetd/server.h         |   1 +
 src/common/setup_before.h  |   1 +
 11 files changed, 338 insertions(+), 2 deletions(-)
 create mode 100644 src/bnetd/i18n.cpp
 create mode 100644 src/bnetd/i18n.h

diff --git a/src/bnetd/CMakeLists.txt b/src/bnetd/CMakeLists.txt
index 05ba464..7007acb 100644
--- a/src/bnetd/CMakeLists.txt
+++ b/src/bnetd/CMakeLists.txt
@@ -36,6 +36,7 @@ set(BNETD_SOURCES
 	anongame_wol.cpp anongame_wol.h handle_wserv.cpp handle_wserv.h
 	luafunctions.cpp luafunctions.h luainterface.cpp luainterface.h 
 	luaobjects.cpp luaobjects.h luawrapper.cpp luawrapper.h
+	i18n.cpp i18n.h
 	../win32/winmain.cpp ../win32/winmain.h
 	)
 	
diff --git a/src/bnetd/command.cpp b/src/bnetd/command.cpp
index e3d3b61..d51d202 100644
--- a/src/bnetd/command.cpp
+++ b/src/bnetd/command.cpp
@@ -3763,7 +3763,9 @@ namespace pvpgn
 			std::string mode_str = args[1];
 
 			if (mode_str == "all")
-				mode = restart_mode_channels;
+				mode = restart_mode_all;
+			else if (mode_str == "i18n")
+				mode = restart_mode_i18n;
 			else if (mode_str == "channels")
 				mode = restart_mode_channels;
 			else if (mode_str == "realms")
diff --git a/src/bnetd/i18n.cpp b/src/bnetd/i18n.cpp
new file mode 100644
index 0000000..683562b
--- /dev/null
+++ b/src/bnetd/i18n.cpp
@@ -0,0 +1,236 @@
+/*
+* Copyright (C) 2014  HarpyWar (harpywar@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 2
+* of the License, or (at your option) 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., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+*/
+#include "common/setup_before.h"
+
+#include <algorithm>
+#include <cctype>
+#include <cstdio>
+#include <cerrno>
+#include <cstring>
+#include <ctime>
+#include <cstdlib>
+#include <sstream>
+#include <iostream>
+#include <fstream>
+#include <vector>
+#include <utility>
+#include <map>
+#include <string.h>
+
+#include "compat/strcasecmp.h"
+#include "compat/snprintf.h"
+
+#include "common/token.h"
+
+#include "common/list.h"
+#include "common/eventlog.h"
+#include "common/xalloc.h"
+#include "common/xstring.h"
+#include "common/util.h"
+#include "common/tag.h"
+#include "common/pugixml.h"
+#include "common/format.h"
+
+#include "account.h"
+#include "connection.h"
+#include "message.h"
+#include "helpfile.h"
+#include "channel.h"
+#include "i18n.h"
+#include "prefs.h"
+#include "common/setup_after.h"
+
+namespace pvpgn
+{
+
+	namespace bnetd
+	{
+		const char * commonfile = "common.xml"; // filename template, actually file name is "common-{lang}.xml"
+
+		/* Array with string translations, each string has array with pair language=translation
+			{ 
+				original = { 
+					{ language = translate }, 
+					... 
+				}, 
+				... 
+			} 
+		*/
+		std::map<std::string, std::map<t_gamelang, std::string> > translations = std::map<std::string, std::map<t_gamelang, std::string> >();
+		
+		std::vector<t_gamelang> languages{
+			GAMELANG_ENGLISH_UINT,	/* enUS */
+			GAMELANG_GERMAN_UINT,	/* deDE */
+			GAMELANG_CZECH_UINT,	/* csCZ */
+			GAMELANG_SPANISH_UINT,	/* esES */
+			GAMELANG_FRENCH_UINT,	/* frFR */
+			GAMELANG_ITALIAN_UINT,	/* itIT */
+			GAMELANG_JAPANESE_UINT,	/* jaJA */
+			GAMELANG_KOREAN_UINT,	/* koKR */
+			GAMELANG_POLISH_UINT,	/* plPL */
+			GAMELANG_RUSSIAN_UINT,	/* ruRU */
+			GAMELANG_CHINESE_S_UINT,	/* zhCN */
+			GAMELANG_CHINESE_T_UINT	/* zhTW */
+		};
+
+		const char * _find_string(char const * text, t_gamelang gamelang);
+
+
+		extern int i18n_reload(void)
+		{
+			translations.clear();
+			i18n_load();
+
+			return 0;
+		}
+
+		extern int i18n_load(void)
+		{
+			const char * filename = buildpath(prefs_get_i18ndir(), commonfile);
+
+			std::string lang_filename;
+			pugi::xml_document doc;
+			std::string original, translate;
+
+			// iterate language list
+			for (std::vector<t_gamelang>::iterator lang = languages.begin(); lang != languages.end(); ++lang)
+			{
+				lang_filename = i18n_filename(filename, *lang);
+				if (FILE *f = fopen(lang_filename.c_str(), "r")) {
+					fclose(f);
+
+					if (!doc.load_file(lang_filename.c_str()))
+					{
+						ERROR1("could not parse localization file \"%s\"", lang_filename.c_str());
+						continue;
+					}
+				}
+				else {
+					// file not exists, ignore it
+					continue;
+				}
+
+				// root node
+				pugi::xml_node nodes = doc.child("strings");
+
+				// load xml strings to map
+				for (pugi::xml_node node = nodes.child("string"); node; node = node.next_sibling("string"))
+				{
+					original = node.child_value("original");
+					if (original[0] == '\0')
+						continue;
+
+					//std::map<const char *, std::map<t_gamelang, const char *> >::iterator it = translations.find(original);
+					// if not found then init
+					//if (it == translations.end())
+					//	translations[original] = std::map<t_gamelang, const char *>();
+					
+
+					// check if translate string has a reference to another translation
+					if (pugi::xml_attribute attr = node.child("translate").attribute("id"))
+					{
+						if (pugi::xml_node n = nodes.find_child_by_attribute("id", attr.value()))
+							translate = n.child_value("translate");
+						else
+						{
+							translate = original;
+							WARN2("could not find localization reference id=\"%s\", use original string (%s)", attr.value(), lang_filename.c_str());
+						}
+					}
+					else
+					{
+						translate = node.child_value("translate");
+						if (translate[0] == '\0')
+						{
+							translate = original;
+							WARN2("empty localization for \"%s\", use original string (%s)", original, lang_filename.c_str());
+						}
+					}
+					translations[original][*lang] = translate;
+				}
+
+				INFO1("localization file loaded \"%s\"", lang_filename.c_str());
+			}
+
+			return 0;
+		}
+
+		/* Find localized text for the given language */
+		const char * _find_string(char const * text, t_gamelang gamelang)
+		{
+			std::map<std::string, std::map<t_gamelang, std::string> >::iterator it = translations.find(text);
+			if (it != translations.end())
+			{
+				if (!it->second[gamelang].empty())
+					return it->second[gamelang].c_str();
+			}
+			return NULL;
+
+		}
+
+		/* Return text translation */
+		extern const char * localize(t_connection * c, char const * text)
+		{
+			t_gamelang gamelang = conn_get_gamelang(c);
+			
+			if (!gamelang || !tag_check_gamelang(gamelang))
+				return text;
+
+			if (const char * translate = _find_string(text, gamelang))
+				return translate;
+
+			return text;
+		}
+
+
+		/* Add a locale tag into filename
+		    example: motd.txt -> motd-ruRU.txt */
+		extern std::string i18n_filename(const char * filename, t_tag gamelang)
+		{
+			// get language string
+			char lang_str[sizeof(t_tag)+1];
+			std::memset(lang_str, 0, sizeof(lang_str));
+			tag_uint_to_str(lang_str, gamelang);
+
+			if (!tag_check_gamelang(gamelang))
+			{
+				ERROR1("got unknown language tag \"%s\"", lang_str);
+				return filename;
+			}
+
+			std::string _filename(filename);
+
+			// get extension
+			std::string::size_type idx(_filename.rfind('.'));
+			if (idx == std::string::npos || idx + 4 != _filename.size())
+			{
+				ERROR1("Invalid extension for '%s'", _filename.c_str());
+				return filename;
+			}
+			std::string ext(_filename.substr(idx + 1));
+
+			// get filename without extension
+			std::string fname(_filename.substr(0, idx));
+
+			std::string lang_filename(fname + "-" + lang_str + "." + ext);
+			return lang_filename;
+		}
+
+
+	}
+}
diff --git a/src/bnetd/i18n.h b/src/bnetd/i18n.h
new file mode 100644
index 0000000..18368c0
--- /dev/null
+++ b/src/bnetd/i18n.h
@@ -0,0 +1,58 @@
+/*
+* Copyright (C) 2014  HarpyWar (harpywar@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 2
+* of the License, or (at your option) 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., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+*/
+#ifndef INCLUDED_LOCALIZATION_TYPES
+#define INCLUDED_LOCALIZATION_TYPES
+
+namespace pvpgn
+{
+	namespace bnetd
+	{
+
+
+	}
+}
+
+#endif
+
+#ifndef JUST_NEED_TYPES
+#ifndef INCLUDED_LOCALIZATION_PROTOS
+#define INCLUDED_LOCALIZATION_PROTOS
+
+#define JUST_NEED_TYPES
+# include <string>
+#undef JUST_NEED_TYPES
+
+namespace pvpgn
+{
+
+	namespace bnetd
+	{
+		extern int i18n_load(void);
+		extern int i18n_reload(void);
+		extern const char * localize(t_connection * c, char const * text);
+
+
+		extern std::string i18n_filename(const char * filename, t_tag gamelang);
+
+	}
+
+}
+
+/*****/
+#endif
+#endif
diff --git a/src/bnetd/luainterface.cpp b/src/bnetd/luainterface.cpp
index 718a424..ff30364 100644
--- a/src/bnetd/luainterface.cpp
+++ b/src/bnetd/luainterface.cpp
@@ -167,6 +167,7 @@ namespace pvpgn
 			{
 				lua::table config = bind.table();
 				config.update("filedir", prefs_get_filedir());
+				config.update("i18ndir", prefs_get_i18ndir());
 				config.update("scriptdir", prefs_get_scriptdir());
 				config.update("reportdir", prefs_get_reportdir());
 				config.update("chanlogdir", prefs_get_chanlogdir());
diff --git a/src/bnetd/main.cpp b/src/bnetd/main.cpp
index fc118fd..3f9c018 100644
--- a/src/bnetd/main.cpp
+++ b/src/bnetd/main.cpp
@@ -89,6 +89,7 @@
 #include "realm.h"
 #include "topic.h"
 #include "handle_apireg.h"
+#include "i18n.h"
 #include "common/setup_after.h"
 
 #ifdef WITH_LUA
@@ -335,6 +336,8 @@ int pre_server_startup(void)
 		return STATUS_FDWATCH_FAILURE;
 	}
 
+	i18n_load();
+
 	connlist_create();
 	gamelist_create();
 	timerlist_create();
@@ -382,7 +385,6 @@ int pre_server_startup(void)
 		eventlog(eventlog_level_error, __FUNCTION__, "could not load realm list");
 	topiclist_load(prefs_get_topicfile());
 
-
 #ifdef WITH_LUA
 	lua_load(prefs_get_scriptdir());
 #endif
diff --git a/src/bnetd/prefs.cpp b/src/bnetd/prefs.cpp
index a27ef5c..ce3ec75 100644
--- a/src/bnetd/prefs.cpp
+++ b/src/bnetd/prefs.cpp
@@ -39,6 +39,7 @@ namespace pvpgn
 		static struct {
 			/* files and paths */
 			char const * filedir;
+			char const * i18ndir;
 			char const * storage_path;
 			char const * logfile;
 			char const * loglevels;
@@ -179,6 +180,10 @@ namespace pvpgn
 		static const char *conf_get_filedir(void);
 		static int conf_setdef_filedir(void);
 
+		static int conf_set_i18ndir(const char *valstr);
+		static const char *conf_get_i18ndir(void);
+		static int conf_setdef_i18ndir(void);
+
 		static int conf_set_storage_path(const char *valstr);
 		static const char *conf_get_storage_path(void);
 		static int conf_setdef_storage_path(void);
@@ -703,6 +708,7 @@ namespace pvpgn
 		static t_conf_entry conf_table[] =
 		{
 			{ "filedir", conf_set_filedir, conf_get_filedir, conf_setdef_filedir },
+			{ "i18ndir", conf_set_i18ndir, conf_get_i18ndir, conf_setdef_i18ndir },
 			{ "storage_path", conf_set_storage_path, conf_get_storage_path, conf_setdef_storage_path },
 			{ "logfile", conf_set_logfile, conf_get_logfile, conf_setdef_logfile },
 			{ "loglevels", conf_set_loglevels, conf_get_loglevels, conf_setdef_loglevels },
@@ -912,6 +918,27 @@ namespace pvpgn
 		}
 
 
+		extern char const * prefs_get_i18ndir(void)
+		{
+			return prefs_runtime_config.i18ndir;
+		}
+
+		static int conf_set_i18ndir(const char *valstr)
+		{
+			return conf_set_str(&prefs_runtime_config.i18ndir, valstr, NULL);
+		}
+
+		static int conf_setdef_i18ndir(void)
+		{
+			return conf_set_str(&prefs_runtime_config.i18ndir, NULL, BNETD_I18N_DIR);
+		}
+
+		static const char* conf_get_i18ndir(void)
+		{
+			return prefs_runtime_config.i18ndir;
+		}
+
+
 		extern char const * prefs_get_logfile(void)
 		{
 			return prefs_runtime_config.logfile;
diff --git a/src/bnetd/prefs.h b/src/bnetd/prefs.h
index 46d3bc1..7655677 100644
--- a/src/bnetd/prefs.h
+++ b/src/bnetd/prefs.h
@@ -37,6 +37,7 @@ namespace pvpgn
 		extern void prefs_unload(void);
 		extern char const * prefs_get_storage_path(void);
 		extern char const * prefs_get_filedir(void);
+		extern char const * prefs_get_i18ndir(void);
 		extern char const * prefs_get_logfile(void);
 		extern char const * prefs_get_loglevels(void);
 		extern char const * prefs_get_motdfile(void);
diff --git a/src/bnetd/server.cpp b/src/bnetd/server.cpp
index 31ef3de..0533503 100644
--- a/src/bnetd/server.cpp
+++ b/src/bnetd/server.cpp
@@ -86,6 +86,7 @@
 #include "icons.h"
 #include "anongame_infos.h"
 #include "topic.h"
+#include "i18n.h"
 #include "common/setup_after.h"
 
 #ifdef WITH_LUA
@@ -1373,6 +1374,11 @@ namespace pvpgn
 						attrlayer_load_default();
 					}
 
+					if (do_restart == restart_mode_all || do_restart == restart_mode_i18n)
+					{
+						i18n_reload();
+					}
+
 					if (do_restart == restart_mode_all || do_restart == restart_mode_channels)
 					{
 						channellist_reload();
diff --git a/src/bnetd/server.h b/src/bnetd/server.h
index 6723efe..b5a763a 100644
--- a/src/bnetd/server.h
+++ b/src/bnetd/server.h
@@ -76,6 +76,7 @@ namespace pvpgn
 			restart_mode_none, // always first (0)
 
 			restart_mode_all,
+			restart_mode_i18n,
 			restart_mode_channels,
 			restart_mode_realms,
 			restart_mode_autoupdate,
diff --git a/src/common/setup_before.h b/src/common/setup_before.h
index bf37070..73daa66 100644
--- a/src/common/setup_before.h
+++ b/src/common/setup_before.h
@@ -126,6 +126,7 @@ const bool BNETD_CHANLOG = false;
 # define BNETD_DEFAULT_CONF_FILE "conf/bnetd.conf"
 #endif
 const char * const BNETD_FILE_DIR = "files";
+const char * const BNETD_I18N_DIR = "conf/i18n";
 const char * const BNETD_SCRIPT_DIR = "lua";
 const char * const BNETD_STORAGE_PATH = "";
 const char * const BNETD_REPORT_DIR = "reports";