1 /**
2  * @file
3  * @brief Language code
4  */
5 
6 /*
7 All original material Copyright (C) 2002-2013 UFO: Alien Invasion.
8 
9 This program is free software; you can redistribute it and/or
10 modify it under the terms of the GNU General Public License
11 as published by the Free Software Foundation; either version 2
12 of the License, or (at your option) any later version.
13 
14 This program is distributed in the hope that it will be useful,
15 but WITHOUT ANY WARRANTY; without even the implied warranty of
16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
17 
18 See the GNU General Public License for more details.
19 
20 You should have received a copy of the GNU General Public License
21 along with this program; if not, write to the Free Software
22 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
23 
24 */
25 
26 #include "client.h"
27 #include "cl_language.h"
28 #include "../shared/parse.h"
29 #include "../ports/system.h"
30 
31 #include "ui/ui_main.h"
32 #include "ui/ui_font.h"
33 #include "ui/node/ui_node_abstractoption.h"
34 
35 static cvar_t* fs_i18ndir;
36 static memPool_t* cl_msgidPool;
37 
38 #define MAX_MSGIDS 512
39 /**
40  * The msgids are reparsed each time that we change the language - we are only
41  * pointing to the po file content to not waste memory for our long texts.
42  */
43 typedef struct msgid_s {
44 	const char* id;				/**< the msgid id used for referencing via *msgid: 'id' */
45 	const char* text;			/**< the pointer to the po file */
46 	struct msgid_s* hash_next;	/**< hash map next pointer in case of collision */
47 } msgid_t;
48 
49 static msgid_t msgIDs[MAX_MSGIDS];
50 static int numMsgIDs;
51 #define MAX_MSGIDHASH 256
52 static msgid_t* msgIDHash[MAX_MSGIDHASH];
53 
54 #define MSGIDSIZE 65536
55 static char* msgIDText;
56 
CL_ParseMessageID(const char * name,const char ** text)57 static void CL_ParseMessageID (const char* name, const char** text)
58 {
59 	/* get it's body */
60 	const char* token = Com_Parse(text);
61 	if (!*text || *token != '{') {
62 		Com_Printf("CL_ParseMessageID: msgid \"%s\" without body ignored\n", name);
63 		return;
64 	}
65 
66 	/* search for game types with same name */
67 	int i;
68 	for (i = 0; i < numMsgIDs; i++)
69 		if (Q_streq(token, msgIDs[i].id))
70 			break;
71 
72 	if (i == numMsgIDs) {
73 		msgid_t* msgid = &msgIDs[numMsgIDs++];
74 
75 		if (numMsgIDs >= MAX_MSGIDS)
76 			Sys_Error("CL_ParseMessageID: MAX_MSGIDS exceeded");
77 
78 		OBJZERO(*msgid);
79 		msgid->id = Mem_PoolStrDup(name, cl_msgidPool, 0);
80 		const unsigned int hash = Com_HashKey(msgid->id, MAX_MSGIDHASH);
81 		HASH_Add(msgIDHash, msgid, hash);
82 
83 		do {
84 			const char* errhead = "CL_ParseMessageID: unexpected end of file (msgid ";
85 			token = Com_EParse(text, errhead, name);
86 			if (!*text)
87 				break;
88 			if (*token == '}')
89 				break;
90 			if (Q_streq(token, "text")) {
91 				/* found a definition */
92 				token = Com_EParse(text, errhead, name, msgIDText, MSGIDSIZE);
93 				if (!*text)
94 					break;
95 				if (token[0] == '_')
96 					token++;
97 				if (token[0] != '\0')
98 					msgid->text = _(token);
99 				else
100 					msgid->text = token;
101 				if (msgid->text == token) {
102 					msgid->text = Mem_PoolStrDup(token, cl_msgidPool, 0);
103 					Com_Printf("no translation for %s\n", msgid->id);
104 				}
105 			}
106 		} while (*text);
107 	} else {
108 		Com_Printf("CL_ParseMessageID: msgid \"%s\" with same already exists - ignore the second one\n", name);
109 		Com_SkipBlock(text);
110 	}
111 }
112 
CL_GetMessageID(const char * id)113 static const char* CL_GetMessageID (const char* id)
114 {
115 	const unsigned int hash = Com_HashKey(id, MAX_MSGIDHASH);
116 	for (msgid_t** anchor = &msgIDHash[hash]; *anchor; anchor = &(*anchor)->hash_next) {
117 		if (Q_streq(id, (*anchor)->id))
118 			return (*anchor)->text;
119 	}
120 	return id;
121 }
122 
CL_Translate(const char * t)123 const char* CL_Translate (const char* t)
124 {
125 	if (t[0] == '_') {
126 		if (t[1] != '\0')
127 			t = _(++t);
128 	} else {
129 		const char* msgid = Q_strstart(t, "*msgid:");
130 		if (msgid != nullptr)
131 			t = CL_GetMessageID(msgid);
132 	}
133 
134 	return t;
135 }
136 
CL_ParseMessageIDs(void)137 void CL_ParseMessageIDs (void)
138 {
139 	const char* type, *name, *text;
140 
141 	numMsgIDs = 0;
142 	OBJZERO(msgIDHash);
143 
144 	if (cl_msgidPool != nullptr) {
145 		Mem_FreePool(cl_msgidPool);
146 	} else {
147 		cl_msgidPool = Mem_CreatePool("msgids");
148 	}
149 	msgIDText = Mem_PoolAllocTypeN(char, MSGIDSIZE, cl_msgidPool);
150 
151 	Com_Printf("\n----------- parse msgids -----------\n");
152 
153 	Com_Printf("%i msgid files\n", FS_BuildFileList("ufos/msgid/*.ufo"));
154 	text = nullptr;
155 
156 	FS_NextScriptHeader(nullptr, nullptr, nullptr);
157 
158 	while ((type = FS_NextScriptHeader("ufos/msgid/*.ufo", &name, &text)) != nullptr) {
159 		if (Q_streq(type, "msgid"))
160 			CL_ParseMessageID(name, &text);
161 	}
162 }
163 
164 /**
165  * @brief List of all mappings for a locale
166  */
167 typedef struct localeMapping_s {
168 	char* localeMapping;	/**< string that contains e.g. en_US.UTF-8 */
169 	struct localeMapping_s* next;	/**< next entry in the linked list */
170 } localeMapping_t;
171 
172 /**
173  * @brief Struct that reflects parsed language definitions
174  * from our script files
175  */
176 typedef struct language_s {
177 	const char* localeID;			/**< short locale id */
178 	const char* localeString;		/**< translatable locale string to show in menus */
179 	const char* nativeString;		/**< Name of the language in the native language itself */
180 	localeMapping_t* localeMapping;	/**< mapping to real locale string for setlocale */
181 	struct language_s* next;	/**< next language in this list */
182 } language_t;
183 
184 static language_t* languageList;	/**< linked list of all parsed languages */
185 static int languageCount; /**< how many languages do we have */
186 
187 /**
188  * @brief Searches the locale script id with the given locale string
189  * @param[in] fullLocale The full locale string. E.g. en_US.UTF-8
190  */
CL_GetLocaleID(const char * fullLocale)191 static const char* CL_GetLocaleID (const char* fullLocale)
192 {
193 	int i;
194 	language_t* language;
195 
196 	for (i = 0, language = languageList; i < languageCount; language = language->next, i++) {
197 		localeMapping_t* mapping = language->localeMapping;
198 
199 		while (mapping) {
200 			if (Q_streq(fullLocale, mapping->localeMapping))
201 				return language->localeID;
202 			mapping = mapping->next;
203 		}
204 	}
205 	Com_DPrintf(DEBUG_CLIENT, "CL_GetLocaleID: Could not find your system locale '%s'. "
206 		"Add it to the languages script file and send a patch please.\n", fullLocale);
207 	return nullptr;
208 }
209 
210 /**
211  * @brief Parse all language definitions from the script files
212  */
CL_ParseLanguages(const char * name,const char ** text)213 void CL_ParseLanguages (const char* name, const char** text)
214 {
215 	const char* errhead = "CL_ParseLanguages: unexpected end of file (language ";
216 	const char	*token;
217 
218 	if (!*text) {
219 		Com_Printf("CL_ParseLanguages: language without body ignored (%s)\n", name);
220 		return;
221 	}
222 
223 	token = Com_EParse(text, errhead, name);
224 	if (!*text || *token != '{') {
225 		Com_Printf("CL_ParseLanguages: language without body ignored (%s)\n", name);
226 		return;
227 	}
228 
229 	language_t* const language = Mem_PoolAllocType(language_t, cl_genericPool);
230 	language->localeID = Mem_PoolStrDup(name, cl_genericPool, 0);
231 	language->localeString = "";
232 	language->nativeString = "";
233 	language->localeMapping = nullptr;
234 
235 	do {
236 		/* get the name type */
237 		token = Com_EParse(text, errhead, name);
238 		if (!*text || *token == '}')
239 			break;
240 		/* inner locale id definition */
241 		if (Q_streq(token, "code")) {
242 			linkedList_t* list;
243 			if (!Com_ParseList(text, &list)) {
244 				Com_Error(ERR_DROP, "CL_ParseLanguages: error while reading language codes \"%s\"", name);
245 			}
246 			for (linkedList_t* element = list; element != nullptr; element = element->next) {
247 				localeMapping_t* const mapping = Mem_PoolAllocType(localeMapping_t, cl_genericPool);
248 				mapping->localeMapping = Mem_PoolStrDup((char*)element->data, cl_genericPool, 0);
249 				/* link it in */
250 				mapping->next = language->localeMapping;
251 				language->localeMapping = mapping;
252 			}
253 			LIST_Delete(&list);
254 		} else if (Q_streq(token, "name")) {
255 			token = Com_EParse(text, errhead, name);
256 			if (!*text || *token == '}')
257 				Com_Error(ERR_FATAL, "CL_ParseLanguages: Name expected for language \"%s\".\n", name);
258 			if (*token != '_') {
259 				Com_Printf("CL_ParseLanguages: language: '%s' - not marked translatable (%s)\n", name, token);
260 			}
261 			language->localeString = Mem_PoolStrDup(token, cl_genericPool, 0);
262 		} else if (Q_streq(token, "native")) {
263 			token = Com_EParse(text, errhead, name);
264 			if (!*text || *token == '}')
265 				Com_Error(ERR_FATAL, "CL_ParseLanguages: Native expected for language \"%s\".\n", name);
266 			language->nativeString = Mem_PoolStrDup(token, cl_genericPool, 0);
267 		}
268 	} while (*text);
269 
270 	language->next = languageList;
271 	languageList = language;
272 	languageCount++;
273 }
274 
275 /**
276  * @brief Test given language by trying to set locale.
277  * @param[in] localeID language abbreviation.
278  * @return true if setting given language is possible.
279  */
CL_LanguageTest(const char * localeID)280 static bool CL_LanguageTest (const char* localeID)
281 {
282 #ifndef _WIN32
283 	int i;
284 	language_t* language;
285 	localeMapping_t* mapping;
286 #endif
287 	char languagePath[MAX_OSPATH];
288 
289 	assert(localeID);
290 
291 	/* Find the proper *.mo file. */
292 	if (fs_i18ndir->string[0] != '\0')
293 		Q_strncpyz(languagePath, fs_i18ndir->string, sizeof(languagePath));
294 	else
295 #ifdef LOCALEDIR
296 		Com_sprintf(languagePath, sizeof(languagePath), LOCALEDIR);
297 #else
298 		Com_sprintf(languagePath, sizeof(languagePath), "%s/" BASEDIRNAME "/i18n/", FS_GetCwd());
299 #endif
300 	Com_DPrintf(DEBUG_CLIENT, "CL_LanguageTest: using mo files from '%s'\n", languagePath);
301 	Q_strcat(languagePath, sizeof(languagePath), "%s/LC_MESSAGES/ufoai.mo", localeID);
302 
303 	/* No *.mo file -> no language. */
304 	if (!FS_FileExists("%s", languagePath)) {
305 		Com_DPrintf(DEBUG_CLIENT, "CL_LanguageTest: locale '%s' not found.\n", localeID);
306 		return false;
307 	}
308 
309 #ifdef _WIN32
310 	if (Sys_Setenv("LANGUAGE=%s", localeID) == 0) {
311 		Com_DPrintf(DEBUG_CLIENT, "CL_LanguageTest: locale '%s' found.\n", localeID);
312 		return true;
313 	}
314 #else
315 	for (i = 0, language = languageList; i < languageCount; language = language->next, i++) {
316 		if (Q_streq(localeID, language->localeID))
317 			break;
318 	}
319 	if (i == languageCount) {
320 		Com_DPrintf(DEBUG_CLIENT, "CL_LanguageTest: Could not find locale with id '%s'\n", localeID);
321 		return false;
322 	}
323 
324 	mapping = language->localeMapping;
325 	if (!mapping) {
326 		Com_DPrintf(DEBUG_CLIENT, "No locale mappings for locale with id '%s'\n", localeID);
327 		return false;
328 	}
329 	/* Cycle through all mappings, but stop at first locale possible to set. */
330 	do {
331 		/* setlocale() will return nullptr if no setting possible. */
332 		if (setlocale(LC_MESSAGES, mapping->localeMapping)) {
333 			Com_DPrintf(DEBUG_CLIENT, "CL_LanguageTest: language '%s' with locale '%s' found.\n", localeID, mapping->localeMapping);
334 			return true;
335 		} else
336 			Com_DPrintf(DEBUG_CLIENT, "CL_LanguageTest: language '%s' with locale '%s' not found on your system.\n", localeID, mapping->localeMapping);
337 		mapping = mapping->next;
338 	} while (mapping);
339 	Com_DPrintf(DEBUG_CLIENT, "CL_LanguageTest: not possible to use language '%s'.\n", localeID);
340 #endif
341 
342 	return false;
343 }
344 
CL_LanguageShutdown(void)345 void CL_LanguageShutdown (void)
346 {
347 	languageCount = 0;
348 	languageList = nullptr;
349 	Mem_DeletePool(cl_msgidPool);
350 	cl_msgidPool = nullptr;
351 	msgIDText = nullptr;
352 	numMsgIDs = 0;
353 	OBJZERO(msgIDHash);
354 }
355 
356 /**
357  * @brief Fills the options language menu node with the parsed language mappings
358  * @sa CL_InitAfter
359  * @sa CL_LocaleSet
360  */
CL_LanguageInit(void)361 void CL_LanguageInit (void)
362 {
363 	int i;
364 	language_t* language;
365 	uiNode_t* languageOption = nullptr;
366 	char systemLanguage[MAX_VAR];
367 
368 	fs_i18ndir = Cvar_Get("fs_i18ndir", "", 0, "System path to language files");
369 
370 	if (s_language->string[0] != '\0') {
371 		Com_Printf("CL_LanguageInit: language settings are stored in configuration: %s\n", s_language->string);
372 		Q_strncpyz(systemLanguage, s_language->string, sizeof(systemLanguage));
373 	} else {
374 		const char* currentLocale = Sys_GetLocale();
375 
376 		if (currentLocale) {
377 			const char* localeID = CL_GetLocaleID(currentLocale);
378 			if (localeID)
379 				Q_strncpyz(systemLanguage, localeID, sizeof(systemLanguage));
380 			else
381 				systemLanguage[0] = '\0';
382 		} else
383 			systemLanguage[0] = '\0';
384 	}
385 
386 	Com_DPrintf(DEBUG_CLIENT, "CL_LanguageInit: system language is: '%s'\n", systemLanguage);
387 
388 	for (i = 0, language = languageList; i < languageCount; language = language->next, i++) {
389 		bool available;
390 		available = Q_streq(language->localeID, "none") || CL_LanguageTest(language->localeID);
391 		uiNode_t* option;
392 #if 0
393 		option = UI_AddOption(&languageOption, "", language->localeString, language->localeID);
394 #else
395 		option = UI_AddOption(&languageOption, "", language->nativeString, language->localeID);
396 #endif
397 		option->disabled = !available;
398 	}
399 
400 	/* sort the list, and register it to the menu */
401 	UI_SortOptions(&languageOption);
402 	UI_RegisterOption(OPTION_LANGUAGES, languageOption);
403 
404 	/* Set to the locale remembered previously. */
405 	CL_LanguageTryToSet(systemLanguage);
406 }
407 
408 /**
409  * @brief Adjust game for new language: reregister fonts, etc.
410  */
CL_NewLanguage(void)411 static void CL_NewLanguage (void)
412 {
413 	R_FontShutdown();
414 	R_FontInit();
415 	UI_InitFonts();
416 	R_FontSetTruncationMarker(_("..."));
417 	CL_ParseMessageIDs();
418 }
419 
420 /**
421  * @brief Cycle through all parsed locale mappings and try to set one after another
422  * @param[in] localeID the locale id parsed from scriptfiles (e.g. en or de [the short id])
423  * @sa CL_LocaleSet
424  */
CL_LanguageTryToSet(const char * localeID)425 bool CL_LanguageTryToSet (const char* localeID)
426 {
427 	int i;
428 	language_t* language;
429 	localeMapping_t* mapping;
430 
431 	assert(localeID);
432 
433 	/* in case of an error we really don't want a flooded console */
434 	s_language->modified = false;
435 
436 	for (i = 0, language = languageList; i < languageCount; language = language->next, i++) {
437 		if (Q_streq(localeID, language->localeID))
438 			break;
439 	}
440 
441 	if (i == languageCount) {
442 		Com_Printf("Could not find locale with id '%s'\n", localeID);
443 		return false;
444 	}
445 
446 	mapping = language->localeMapping;
447 	if (!mapping) {
448 		Com_Printf("No locale mappings for locale with id '%s'\n", localeID);
449 		return false;
450 	}
451 
452 	Cvar_Set("s_language", "%s", localeID);
453 	s_language->modified = false;
454 
455 	do {
456 		Com_DPrintf(DEBUG_CLIENT, "CL_LanguageTryToSet: %s (%s)\n", mapping->localeMapping, localeID);
457 		if (Sys_SetLocale(mapping->localeMapping)) {
458 			CL_NewLanguage();
459 			return true;
460 		}
461 		mapping = mapping->next;
462 	} while (mapping);
463 
464 #ifndef _WIN32
465 	Com_DPrintf(DEBUG_CLIENT, "CL_LanguageTryToSet: Finally try: '%s'\n", localeID);
466 	Sys_SetLocale(localeID);
467 	CL_NewLanguage();
468 #endif
469 
470 	return false;
471 }
472