1 /*
2    Copyright (C) 2003 - 2018 by David White <dave@whitevine.net>
3    Part of the Battle for Wesnoth Project https://www.wesnoth.org/
4 
5    This program is free software; you can redistribute it and/or modify
6    it under the terms of the GNU General Public License as published by
7    the Free Software Foundation; either version 2 of the License, or
8    (at your option) any later version.
9    This program is distributed in the hope that it will be useful,
10    but WITHOUT ANY WARRANTY.
11 
12    See the COPYING file for more details.
13 */
14 
15 #include "filesystem.hpp"
16 #include "gettext.hpp"
17 #include "language.hpp"
18 #include "log.hpp"
19 #include "preferences/general.hpp"
20 #include "serialization/parser.hpp"
21 #include "serialization/preprocessor.hpp"
22 
23 #include <stdexcept>
24 #include <clocale>
25 
26 #ifdef _WIN32
27 #include <windows.h>
28 #if !defined(_MSC_VER) && !defined(__MINGW32__)
29 extern "C" int _putenv(const char*);
30 #endif
31 #endif
32 
33 #ifdef __APPLE__
34 #include <cerrno>
35 #endif
36 
37 #define DBG_G LOG_STREAM(debug, lg::general())
38 #define LOG_G LOG_STREAM(info, lg::general())
39 #define WRN_G LOG_STREAM(warn, lg::general())
40 #define ERR_G LOG_STREAM(err, lg::general())
41 
42 namespace {
43 	language_def current_language;
44 	std::vector<config> languages_;
45 	utils::string_map strings_;
46 	int min_translation_percent = 80;
47 }
48 
49 static language_list known_languages;
50 
51 bool load_strings(bool complain);
52 
current_language_rtl()53 bool current_language_rtl()
54 {
55 	return get_language().rtl;
56 }
57 
operator ==(const language_def & a) const58 bool language_def::operator== (const language_def& a) const
59 {
60 	return ((language == a.language) /* && (localename == a.localename) */ );
61 }
62 
63 symbol_table string_table;
64 
time_locale_correct()65 bool& time_locale_correct()
66 {
67 	static bool result = true;
68 	return result;
69 }
70 
operator [](const std::string & key) const71 const t_string& symbol_table::operator[](const std::string& key) const
72 {
73 	const utils::string_map::const_iterator i = strings_.find(key);
74 	if(i != strings_.end()) {
75 		return i->second;
76 	} else {
77 		static t_string empty_string;
78 		// Let's do it the painful way (untlb means untranslatABLE).
79 		// It will cause problem if somebody stores more than one reference at once
80 		// but I don't really care since this path is an error path and it should
81 		// not have been taken in the first place. -- silene
82 		empty_string = "UNTLB " + key;
83 		return empty_string;
84 	}
85 }
86 
operator [](const char * key) const87 const t_string& symbol_table::operator[](const char* key) const
88 {
89 	return (*this)[std::string(key)];
90 }
91 
load_language_list()92 bool load_language_list()
93 {
94 	config cfg;
95 	try {
96 		filesystem::scoped_istream stream = preprocess_file(filesystem::get_wml_location("hardwired/language.cfg"));
97 		read(cfg, *stream);
98 	} catch(const config::error &) {
99 		return false;
100 	}
101 
102 	known_languages.clear();
103 	known_languages.emplace_back("", t_string(N_("System default language"), "wesnoth"), "ltr", "", "A", "100");
104 
105 	for (const config &lang : cfg.child_range("locale"))
106 	{
107 		known_languages.emplace_back(
108 			lang["locale"], lang["name"], lang["dir"],
109 			lang["alternates"], lang["sort_name"], lang["percent"]);
110 	}
111 
112 	return true;
113 }
114 
get_languages()115 language_list get_languages()
116 {
117 	// We sort every time, the local might have changed which can modify the
118 	// sort order.
119 	std::sort(known_languages.begin(), known_languages.end());
120 
121 	if(min_translation_percent == 0) {
122 		return known_languages;
123 	}
124 
125 	language_list result;
126 	std::copy_if(known_languages.begin(), known_languages.end(), std::back_inserter(result),
127 		[](const language_def& lang) { return lang.percent >= min_translation_percent; });
128 
129 	return result;
130 }
131 
set_min_translation_percent(int percent)132 void set_min_translation_percent(int percent) {
133 	min_translation_percent = percent;
134 }
135 
wesnoth_setlocale(int category,const std::string & slocale,std::vector<std::string> const * alternates)136 static void wesnoth_setlocale(int category, const std::string& slocale,
137 	std::vector<std::string> const *alternates)
138 {
139 	std::string locale = slocale;
140 	// FIXME: ideally we should check LANGUAGE and on first invocation
141 	// use that value, so someone with es would get the game in Spanish
142 	// instead of en_US the first time round
143 	// LANGUAGE overrides other settings, so for now just get rid of it
144 	// FIXME: add configure check for unsetenv
145 
146 	//category is never LC_MESSAGES since that case was moved to gettext.cpp to remove the dependency to libintl.h in this file
147 	//that's why code like if (category == LC_MESSAGES) is outcommented here.
148 #ifndef _WIN32
149 	unsetenv ("LANGUAGE"); // void so no return value to check
150 #endif
151 #ifdef __APPLE__
152 	//if (category == LC_MESSAGES && setenv("LANG", locale.c_str(), 1) == -1) {
153 	//	ERR_G << "setenv LANG failed: " << strerror(errno);
154 	//}
155 #endif
156 
157 #ifdef _WIN32
158 	std::string win_locale(locale, 0, 2);
159 	#include "language_win32.ii"
160 	//if(category == LC_MESSAGES) {
161 	//	SetEnvironmentVariableA("LANG", win_locale.c_str());
162 	//	std::string env = "LANGUAGE=" + locale;
163 	//	_putenv(env.c_str());
164 	//	return;
165 	//}
166 	locale = win_locale;
167 #endif
168 
169 	char *res = nullptr;
170 	std::vector<std::string>::const_iterator i;
171 	if (alternates) i = alternates->begin();
172 
173 	for (;;)
174 	{
175 		std::string lang = locale, extra;
176 		std::string::size_type pos = locale.find('@');
177 		if (pos != std::string::npos) {
178 			lang.erase(pos);
179 			extra = locale.substr(pos);
180 		}
181 
182 		/*
183 		 * The "" is the last item to work-around a problem in glibc picking
184 		 * the non utf8 locale instead an utf8 version if available.
185 		 */
186 		char const *encoding[] { ".utf-8", ".UTF-8", "" };
187 		for (int j = 0; j != 3; ++j)
188 		{
189 			locale = lang + encoding[j] + extra;
190 			res = std::setlocale(category, locale.c_str());
191 			if (res) {
192 				LOG_G << "Set locale to '" << locale << "' result: '" << res << "'.\n";
193 				goto done;
194 			}
195 		}
196 
197 		if (!alternates || i == alternates->end()) break;
198 		locale = *i;
199 		++i;
200 	}
201 
202 	WRN_G << "setlocale() failed for '" << slocale << "'." << std::endl;
203 
204 	if (category == LC_TIME) {
205 		time_locale_correct() = false;
206 	}
207 
208 #ifndef _WIN32
209 		//if(category == LC_MESSAGES) {
210 		//	WRN_G << "Setting LANGUAGE to '" << slocale << "'." << std::endl;
211 		//	setenv("LANGUAGE", slocale.c_str(), 1);
212 		//	std::setlocale(LC_MESSAGES, "");
213 		//}
214 #endif
215 
216 	done:
217 	DBG_G << "Numeric locale: " << std::setlocale(LC_NUMERIC, nullptr) << '\n';
218 	DBG_G << "Full locale: " << std::setlocale(LC_ALL, nullptr) << '\n';
219 }
220 
set_language(const language_def & locale)221 void set_language(const language_def& locale)
222 {
223 	strings_.clear();
224 
225 	std::string locale_lc;
226 	locale_lc.resize(locale.localename.size());
227 	std::transform(locale.localename.begin(),locale.localename.end(),locale_lc.begin(),tolower);
228 
229 	current_language = locale;
230 	time_locale_correct() = true;
231 
232 	wesnoth_setlocale(LC_COLLATE, locale.localename, &locale.alternates);
233 	wesnoth_setlocale(LC_TIME, locale.localename, &locale.alternates);
234 	translation::set_language(locale.localename, &locale.alternates);
235 	load_strings(false);
236 }
237 
load_strings(bool complain)238 bool load_strings(bool complain)
239 {
240 	DBG_G << "Loading strings\n";
241 	config cfg;
242 
243 	LOG_G << "There are " << languages_.size() << " [language] blocks\n";
244 	if (complain && languages_.empty()) {
245 		std::cerr << "No [language] block found\n";
246 		return false;
247 	}
248 	for (const config &lang : languages_) {
249 		DBG_G << "[language]\n";
250 		for (const config::attribute &j : lang.attribute_range()) {
251 			DBG_G << j.first << "=\"" << j.second << "\"\n";
252 			strings_[j.first] = j.second;
253 		}
254 		DBG_G << "[/language]\n";
255 	}
256 	DBG_G << "done\n";
257 
258 	return true;
259 }
260 
get_language()261 const language_def& get_language() { return current_language; }
262 
get_locale()263 const language_def& get_locale()
264 {
265 	//TODO: Add in support for querying the locale on Windows
266 
267 	assert(!known_languages.empty());
268 
269 	const std::string& prefs_locale = preferences::language();
270 	if(prefs_locale.empty() == false) {
271 		translation::set_language(prefs_locale, nullptr);
272 		for(language_list::const_iterator i = known_languages.begin();
273 				i != known_languages.end(); ++i) {
274 			if (prefs_locale == i->localename)
275 				return *i;
276 		}
277 		LOG_G << "'" << prefs_locale << "' locale not found in known array; defaulting to system locale\n";
278 		return known_languages[0];
279 	}
280 
281 #if 0
282 	const char* const locale = getenv("LANG");
283 	#ifdef _WIN32
284 	    std::string win_locale = locale
285 		#include "language_win32.ii"
286 		return win_locale;
287 	#endif
288 	if(locale != nullptr && strlen(locale) >= 2) {
289 		//we can't pass pointers into the string to the std::string
290 		//constructor because some STL implementations don't support
291 		//it (*cough* MSVC++6)
292 		std::string res(2,'z');
293 		res[0] = tolower(locale[0]);
294 		res[1] = tolower(locale[1]);
295 		return res;
296 	}
297 #endif
298 
299 	LOG_G << "locale could not be determined; defaulting to system locale\n";
300 	return known_languages[0];
301 }
302 
init_textdomains(const config & cfg)303 void init_textdomains(const config& cfg)
304 {
305 	for (const config &t : cfg.child_range("textdomain"))
306 	{
307 		const std::string &name = t["name"];
308 		const std::string &path = t["path"];
309 
310 		if(path.empty()) {
311 			t_string::add_textdomain(name, filesystem::get_intl_dir());
312 		} else {
313 			std::string location = filesystem::get_binary_dir_location("", path);
314 
315 			if (location.empty()) {
316 				//if location is empty, this causes a crash on Windows, so we
317 				//disallow adding empty domains
318 				WRN_G << "no location found for '" << path << "', skipping textdomain" << std::endl;
319 			} else {
320 				t_string::add_textdomain(name, location);
321 			}
322 		}
323 	}
324 }
325 
init_strings(const config & cfg)326 bool init_strings(const config& cfg)
327 {
328 	languages_.clear();
329 	for (const config &l : cfg.child_range("language")) {
330 		languages_.push_back(l);
331 	}
332 	return load_strings(true);
333 }
334 
335 /* vim:set encoding=utf-8: */
336