1 /*
2  * Copyright (C) 2006-2020 by the Widelands Development Team
3  *
4  * This program is free software; you can redistribute it and/or
5  * modify it under the terms of the GNU General Public License
6  * as published by the Free Software Foundation; either version 2
7  * of the License, or (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program; if not, write to the Free Software
16  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17  *
18  */
19 
20 #include "base/i18n.h"
21 
22 #ifdef __FreeBSD__
23 #include <clocale>
24 #endif
25 
26 #include <map>
27 
28 #include <boost/format.hpp>
29 
30 #include "base/log.h"
31 #include "config.h"
32 
33 #ifdef __APPLE__
34 #if LIBINTL_VERSION >= 0x001201
35 // for older libintl versions, setlocale is just fine
36 #define SETLOCALE libintl_setlocale
37 #endif  // LIBINTL_VERSION
38 #else   // __APPLE__
39 #if defined _WIN32
40 #define SETLOCALE setlocale
41 #else
42 #define SETLOCALE std::setlocale
43 #endif
44 #endif
45 
46 extern int _nl_msg_cat_cntr;
47 
48 namespace i18n {
49 
50 /// A stack of textdomains. On entering a new textdomain, the old one gets
51 /// pushed on the stack. On leaving the domain again it is popped back.
52 /// \see grab_texdomain()
53 namespace {
54 
55 std::vector<std::pair<std::string, std::string>> textdomains;
56 
57 std::string env_locale;
58 std::string locale;
59 std::string localedir;
60 
61 }  // namespace
62 
63 /**
64  * Translate a string with gettext
65  */
66 // TODO(unknown): Implement a workaround if gettext was not found
translate(char const * const str)67 char const* translate(char const* const str) {
68 	return gettext(str);
69 }
translate(const std::string & str)70 char const* translate(const std::string& str) {
71 	return gettext(str.c_str());
72 }
73 
74 /**
75  * Set the localedir. This should usually only be done once
76  */
set_localedir(const std::string & dname)77 void set_localedir(const std::string& dname) {
78 	localedir = dname;
79 }
80 
get_localedir()81 const std::string& get_localedir() {
82 	return localedir;
83 }
84 
85 /**
86  * Grab a given TextDomain. If a new one is grabbed, it is pushed on the stack.
87  * On release, it is dropped and the previous one is re-grabbed instead.
88  *
89  * So when a tribe loads, it grabs its textdomain, loads all data and releases
90  * it -> we're back in widelands domain. Negative: We can't translate error
91  * messages. Who cares?
92  */
grab_textdomain(const std::string & domain)93 void grab_textdomain(const std::string& domain) {
94 	char const* const dom = domain.c_str();
95 	char const* const ldir = localedir.c_str();
96 
97 	bindtextdomain(dom, ldir);
98 	bind_textdomain_codeset(dom, "UTF-8");
99 	// log("textdomain %s @ %s\n", dom, ldir);
100 	textdomain(dom);
101 	textdomains.push_back(std::make_pair(dom, ldir));
102 }
103 
104 /**
105  * See grab_textdomain()
106  */
release_textdomain()107 void release_textdomain() {
108 	if (textdomains.empty()) {
109 		log("ERROR: trying to pop textdomain from empty stack");
110 		return;
111 	}
112 	textdomains.pop_back();
113 
114 	// Don't try to get the previous TD when the very first one ('widelands')
115 	// just got dropped
116 	if (!textdomains.empty()) {
117 		char const* const domain = textdomains.back().first.c_str();
118 
119 		bind_textdomain_codeset(domain, "UTF-8");
120 		bindtextdomain(domain, textdomains.back().second.c_str());
121 		textdomain(domain);
122 	}
123 }
124 
125 /**
126  * Initialize locale to English.
127  * Save system language (for later selection).
128  * Code inspired by wesnoth.org
129  */
init_locale()130 void init_locale() {
131 	env_locale = std::string();
132 #ifdef _WIN32
133 	locale = "English";
134 	SETLOCALE(LC_ALL, "English");
135 #else
136 	// first, save environment variable
137 	char* lang;
138 	lang = getenv("LANG");
139 	if (lang != nullptr) {
140 		env_locale = lang;
141 	}
142 	if (env_locale.empty()) {
143 		lang = getenv("LANGUAGE");
144 		if (lang != nullptr) {
145 			env_locale = lang;
146 		} else {  // Finall fallback in case we cannot find out anything (#1784495)
147 			env_locale = "en";
148 		}
149 	}
150 	locale = "C";
151 	SETLOCALE(LC_ALL, "C");
152 	SETLOCALE(LC_MESSAGES, "");
153 #endif
154 }
155 
156 /**
157  * Set the locale to the given string.
158  * Code inspired by wesnoth.org
159  */
set_locale(const std::string & name)160 void set_locale(const std::string& name) {
161 	const std::map<std::string, std::string> kAlternatives = {
162 	   {"ar", "ar,ar_AR,ar_AE,ar_BH,ar_DZ,ar_EG,ar_IN,ar_IQ,ar_JO,ar_KW,ar_LB,ar_LY,ar_MA,ar_OM,ar_"
163 	          "QA,ar_SA,ar_SD,ar_SY,ar_TN,ar_YE"},
164 	   {"ast", "ast,ast_ES"},
165 	   {"ca", "ca,ca_ES,ca_ES@valencia,ca_FR,ca_IT"},
166 	   {"cs", "cs,cs_CZ"},
167 	   {"da", "da,da_DK"},
168 	   {"de", "de,de_DE,de_AT,de_CH,de_LI,de_LU,de_BE"},
169 	   {"el", "el,el_GR,el_CY"},
170 	   {"en", "en,en_US,en_GB,en_AU,en_CA,en_AG,en_BW,en_DK,en_HK,en_IE,en_IN,en_NG,en_NZ,en_PH,en_"
171 	          "SG,en_ZA,en_ZW"},
172 	   {"en_AU", "en_AU,en,en_US,en_GB"},
173 	   {"en_CA", "en_CA,en,en_US,en_GB"},
174 	   {"en_GB", "en_GB,en,en_US"},
175 	   {"eo", "eo,eo_XX"},
176 	   {"es", "es,es_ES,es_MX,es_US"},
177 	   {"et", "et,et_EE"},
178 	   {"eu", "eu,eu_ES,eu_FR"},
179 	   {"fa", "fa,fa_IR"},
180 	   {"fi", "fi,fi_FI"},
181 	   {"fr", "fr,fr_FR,fr_CH,fr_BE,fr_CA,fr_LU"},
182 	   {"gd", "gd,gd_GB,gd_CA"},
183 	   {"gl", "ga,gl_ES"},
184 	   {"he", "he,he_IL"},
185 	   {"hr", "hr,hr_HR,hr_RS,hr_BA,hr_ME,hr_HU"},
186 	   {"hu", "hu,hu_HU"},
187 	   {"ia", "ia"},
188 	   {"id", "id,id_ID"},
189 	   {"it", "it,it_IT,it_CH"},
190 	   {"ja", "ja,ja_JP"},
191 	   {"jv", "jv,jv_ID,jv_MY,jv_SR,jv_NC"},
192 	   {"ka", "ka,ka_GE"},
193 	   {"ko", "ko,ko_KR"},
194 	   {"la", "la,la_AU,la_VA"},
195 	   {"mr", "mr,mr_IN"},
196 	   {"ms", "ms,ms_MY"},
197 	   {"my", "my,my_MM"},
198 	   {"nb", "nb,nb_NO"},
199 	   {"nl", "nl,nl_NL,nl_BE,nl_AW"},
200 	   {"nn", "nn,nn_NO"},
201 	   {"oc", "oc,oc_FR"},
202 	   {"pl", "pl,pl_PL"},
203 	   {"pt", "pt,pt_PT,pt_BR"},
204 	   {"pt_BR", "pt_BR,pt,pt_PT"},
205 	   {"ru", "ru,ru_RU,ru_UA"},
206 	   {"si", "si,si_LK"},
207 	   {"sk", "sk,sk_SK"},
208 	   {"sl", "sl,sl_SI"},
209 	   {"sr", "sr,sr_RS,sr_ME"},
210 	   {"sv", "sv,sv_FI,sv_SE"},
211 	   {"tr", "tr,tr_TR,tr_CY"},
212 	   {"uk", "uk,uk_UA"},
213 	   {"vi", "vi,vi_VN"},
214 	   {"zh", "zh,zh_CN,zh_TW,zh_HK,zh_MO"},
215 	   {"zh_CN", "zh_CN,zh,zh_TW,zh_HK,zh_MO"},
216 	   {"zh_TW", "zh_TW,zh_HK,zh_MO,zh,zh_CN"},
217 	};
218 
219 	std::string lang(name);
220 
221 	log("selected language: %s\n", lang.empty() ? "(system language)" : lang.c_str());
222 
223 #ifndef _WIN32
224 #ifndef __AMIGAOS4__
225 #ifndef __APPLE__
226 	unsetenv("LANGUAGE");  // avoid problems with this variable
227 #endif
228 #endif
229 #endif
230 
231 	std::string alt_str;
232 	if (lang.empty()) {
233 		// reload system language, if selected
234 		lang = env_locale;
235 		alt_str = env_locale;
236 	} else {
237 		alt_str = lang;
238 		// otherwise, try alternatives.
239 		if (kAlternatives.count(lang)) {
240 			alt_str = kAlternatives.at(lang);
241 		}
242 	}
243 	alt_str += ",";
244 
245 // Somehow setlocale doesn't behave same on
246 // some systems.
247 #ifdef __BEOS__
248 	setenv("LANG", lang.c_str(), 1);
249 	setenv("LC_ALL", lang.c_str(), 1);
250 	locale = lang;
251 #endif
252 #ifdef __APPLE__
253 	setenv("LANGUAGE", lang.c_str(), 1);
254 	setenv("LANG", lang.c_str(), 1);
255 	setenv("LC_ALL", lang.c_str(), 1);
256 	locale = lang;
257 #endif
258 #ifdef _WIN32
259 	_putenv_s("LANG", lang.c_str());
260 	locale = lang;
261 #endif
262 
263 #ifdef __linux__
264 	char* res = nullptr;
265 	char const* encoding[] = {"", ".utf-8", "@euro", ".UTF-8"};
266 	std::size_t found = alt_str.find(',', 0);
267 	bool leave_while = false;
268 	// try every possible combination of alternative and encoding
269 	while (found != std::string::npos) {
270 		std::string base_locale = alt_str.substr(0, int(found));
271 		alt_str = alt_str.erase(0, int(found) + 1);
272 
273 		for (int j = 0; j < 4; ++j) {
274 			std::string try_locale = base_locale + encoding[j];
275 			res = SETLOCALE(LC_MESSAGES, try_locale.c_str());
276 			if (res) {
277 				locale = try_locale;
278 				log("using locale %s\n", try_locale.c_str());
279 				leave_while = true;
280 				break;
281 			} else {
282 				// log("locale is not working: %s\n", try_locale.c_str());
283 			}
284 		}
285 		if (leave_while) {
286 			break;
287 		}
288 
289 		found = alt_str.find(',', 0);
290 	}
291 	if (leave_while) {
292 		setenv("LC_ALL", locale.c_str(), 1);
293 		setenv("LANG", locale.c_str(), 1);
294 		setenv("LANGUAGE", locale.c_str(), 1);
295 	} else {
296 		log("No corresponding locale found\n");
297 		log(" - Set LANGUAGE, LANG and LC_ALL to '%s'\n", lang.c_str());
298 
299 		setenv("LANGUAGE", lang.c_str(), 1);
300 		setenv("LANG", lang.c_str(), 1);
301 		setenv("LC_ALL", lang.c_str(), 1);
302 
303 		try {
304 			SETLOCALE(LC_MESSAGES, "en_US.utf8");  // set locale according to the env. variables
305 			                                       // --> see  $ man 3 setlocale
306 			log(" - Set system locale to 'en_US.utf8' to make '%s' accessible to libintl\n",
307 			    lang.c_str());
308 		} catch (std::exception&) {
309 			SETLOCALE(LC_MESSAGES, "");  // set locale according to the env. variables
310 			                             // --> see  $ man 3 setlocale
311 		}
312 		// assume that it worked
313 		// maybe, do another check with the return value (?)
314 		locale = lang;
315 	}
316 
317 	/* Finally make changes known.  */
318 	++_nl_msg_cat_cntr;
319 #endif
320 
321 	SETLOCALE(LC_ALL, "");  //  call to libintl
322 
323 	if (!textdomains.empty()) {
324 		char const* const domain = textdomains.back().first.c_str();
325 
326 		bind_textdomain_codeset(domain, "UTF-8");
327 		bindtextdomain(domain, textdomains.back().second.c_str());
328 		textdomain(domain);
329 	}
330 }
331 
get_locale()332 const std::string& get_locale() {
333 	return locale;
334 }
335 
localize_list(const std::vector<std::string> & items,ConcatenateWith listtype)336 std::string localize_list(const std::vector<std::string>& items, ConcatenateWith listtype) {
337 	i18n::Textdomain td("widelands");
338 	std::string result;
339 	for (std::vector<std::string>::const_iterator it = items.begin(); it != items.end(); ++it) {
340 		if (it == items.begin()) {
341 			result = *it;
342 		} else if (it == --items.end()) {
343 			if (listtype == ConcatenateWith::AMPERSAND) {
344 				/** TRANSLATORS: Concatenate the last 2 items on a list. */
345 				/** TRANSLATORS: RTL languages might want to change the word order here. */
346 				result = (boost::format(_("%1$s & %2$s")) % result % (*it)).str();
347 			} else if (listtype == ConcatenateWith::OR) {
348 				/** TRANSLATORS: Join the last 2 items on a list with "or". */
349 				/** TRANSLATORS: RTL languages might want to change the word order here. */
350 				result = (boost::format(_("%1$s or %2$s")) % result % (*it)).str();
351 			} else if (listtype == ConcatenateWith::COMMA) {
352 				/** TRANSLATORS: Join the last 2 items on a list with a comma. */
353 				/** TRANSLATORS: RTL languages might want to change the word order here. */
354 				result = (boost::format(_("%1$s, %2$s")) % result % (*it)).str();
355 			} else {
356 				/** TRANSLATORS: Concatenate the last 2 items on a list. */
357 				/** TRANSLATORS: RTL languages might want to change the word order here. */
358 				result = (boost::format(_("%1$s and %2$s")) % result % (*it)).str();
359 			}
360 		} else {
361 			/** TRANSLATORS: Concatenate 2 items at in the middle of a list. */
362 			/** TRANSLATORS: RTL languages might want to change the word order here. */
363 			result = (boost::format(_("%1$s, %2$s")) % result % (*it)).str();
364 		}
365 	}
366 	return result;
367 }
368 
join_sentences(const std::string & sentence1,const std::string & sentence2)369 std::string join_sentences(const std::string& sentence1, const std::string& sentence2) {
370 	i18n::Textdomain td("widelands");
371 	/** TRANSLATORS: Put 2 sentences one after the other. Languages using Chinese script probably
372 	 * want to lose the blank space here. */
373 	return (boost::format(pgettext("sentence_separator", "%1% %2%")) % sentence1 % sentence2).str();
374 }
375 
376 }  // namespace i18n
377