1 /***************************************************************************
2 * Mechanized Assault and Exploration Reloaded Projectfile *
3 * *
4 * This program is free software; you can redistribute it and/or modify *
5 * it under the terms of the GNU General Public License as published by *
6 * the Free Software Foundation; either version 2 of the License, or *
7 * (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 *
16 * Free Software Foundation, Inc., *
17 * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
18 ***************************************************************************/
19 ////////////////////////////////////////////////////////////////////////////////
20 //
21 // File: language.cpp
22 // Date: 07-10-01
23 // Author: JCK
24 //
25 ////////////////////////////////////////////////////////////////////////////////
26 // Description:
27 // This class handles the support for language packs in XML-format.
28 ////////////////////////////////////////////////////////////////////////////////
29
30 #include "utility/language.h"
31 #include "maxrversion.h"
32 #include "extendedtinyxml.h"
33 #include "utility/log.h"
34 #include "main.h"
35 #include "settings.h"
36 #include "utility/files.h"
37 #include "tinyxml2.h"
38 #include "utility/string/toupper.h"
39
40 using namespace tinyxml2;
41
cLanguage()42 cLanguage::cLanguage() :
43 m_bLeftToRight (true),
44 m_bErrorMsgTranslationLoaded (false)
45 {}
46
GetCurrentLanguage() const47 const std::string& cLanguage::GetCurrentLanguage() const
48 {
49 return m_szLanguage;
50 }
51
SetCurrentLanguage(const std::string & szLanguageCode)52 int cLanguage::SetCurrentLanguage (const std::string& szLanguageCode)
53 {
54 // don't do this in constructor
55 // because language folder isn't known yet in programm start.
56 // since the first thing we do with language files is setting
57 // our language we can init the master lang file here too -- beko
58 m_szLanguageFileMaster = LANGUAGE_FILE_FOLDER;
59 m_szLanguageFileMaster += PATH_DELIMITER LANGUAGE_FILE_NAME "eng" LANGUAGE_FILE_EXT;
60
61 if (szLanguageCode.length() != 3)
62 {
63 return -1;
64 }
65 if (szLanguageCode.find_first_not_of
66 ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz") != std::string::npos)
67 {
68 return -1;
69 }
70
71 m_szLanguage = szLanguageCode;
72 for (int i = 0; i <= 2; i++)
73 {
74 if (m_szLanguage[i] < 'a')
75 {
76 m_szLanguage[i] += 'a' - 'A';
77 }
78 }
79
80 m_szLanguageFile = LANGUAGE_FILE_FOLDER;
81 m_szLanguageFile += PATH_DELIMITER LANGUAGE_FILE_NAME;
82 m_szLanguageFile += m_szLanguage + LANGUAGE_FILE_EXT;
83 return 0;
84 }
85
i18n(const std::string & szInputText)86 std::string cLanguage::i18n (const std::string& szInputText)
87 {
88 StrStrMap::const_iterator impTranslation;
89 impTranslation = m_mpLanguage.find (szInputText);
90
91 if (impTranslation == m_mpLanguage.end())
92 {
93 if (m_bErrorMsgTranslationLoaded)
94 {
95 return i18n ("Text~Error_Messages~ERROR_Missing_Translation", szInputText);
96 }
97 else
98 {
99 return std::string ("missing translation: ") + szInputText;
100 }
101 }
102 else
103 {
104 return impTranslation->second;
105 }
106 }
107
108 // Translation with replace %s
i18n(const std::string & szMainText,const std::string & szInsertText)109 std::string cLanguage::i18n (const std::string& szMainText, const std::string& szInsertText)
110 {
111 std::string szMainTextNew;
112 std::size_t iPos;
113
114 szMainTextNew = this->i18n (szMainText);
115 iPos = szMainTextNew.find ("%s");
116 if (iPos == std::string::npos)
117 {
118 Log.write ("Found no place holder in language string. Update language file!", cLog::eLOG_TYPE_WARNING);
119 Log.write ("*-> String in question is: \"" + szMainText + "\"", cLog::eLOG_TYPE_WARNING);
120 return szMainTextNew + szInsertText;
121 }
122 else
123 {
124 szMainTextNew.replace (iPos, 2, szInsertText);
125 return szMainTextNew;
126 }
127 }
128
ReadLanguagePack()129 int cLanguage::ReadLanguagePack()
130 {
131 // First let's load the English language pack and use it as master
132 if (ReadLanguagePackFooter() != 0)
133 {
134 return -1;
135 }
136
137 // Read the complete <Text> - section
138 XMLElement* xmlStartingElement = nullptr;
139 try
140 {
141 xmlStartingElement = XmlGetFirstElement (m_XmlDoc, XNP_MAX_LANG_FILE_TEXT);
142 if (xmlStartingElement == nullptr) throw std::string ("No <Text> section found!");
143
144 xmlStartingElement = xmlStartingElement->FirstChildElement();
145 if (xmlStartingElement == nullptr) throw std::string ("<Text> section is empty!");
146 }
147 catch (const std::string& strMsg)
148 {
149 Log.write ("Language file (eng): " + strMsg, cLog::eLOG_TYPE_ERROR);
150 return -1;
151 }
152 ReadRecursiveLanguagePack (xmlStartingElement, "Text");
153
154 // Read the complete <Graphic> - section
155 xmlStartingElement = nullptr;
156 try
157 {
158 xmlStartingElement = XmlGetFirstElement (m_XmlDoc, XNP_MAX_LANG_FILE_GRAPHIC);
159 if (xmlStartingElement == nullptr) throw std::string ("No <Graphic> section found!");
160
161 xmlStartingElement = xmlStartingElement->FirstChildElement();
162 if (xmlStartingElement == nullptr) throw std::string ("<Graphic> section is empty!");
163 }
164 catch (const std::string& strMsg)
165 {
166 Log.write ("Language file (eng): " + strMsg, cLog::eLOG_TYPE_ERROR);
167 return -1;
168 }
169 ReadRecursiveLanguagePack (xmlStartingElement, "Graphic");
170
171 // Read the complete <Speech> - section
172 xmlStartingElement = nullptr;
173 try
174 {
175 xmlStartingElement = XmlGetFirstElement (m_XmlDoc, XNP_MAX_LANG_FILE_SPEECH);
176 if (xmlStartingElement == nullptr) throw std::string ("No <Speech> section found!");
177
178 xmlStartingElement = xmlStartingElement->FirstChildElement();
179 if (xmlStartingElement == nullptr) throw std::string ("<Speech> section is empty!");
180 }
181 catch (const std::string& strMsg)
182 {
183 Log.write ("Language file (eng): " + strMsg, cLog::eLOG_TYPE_ERROR);
184 return -1;
185 }
186 ReadRecursiveLanguagePack (xmlStartingElement, "Speech");
187
188 // Second step: Load the selected language pack
189 if (m_szLanguage == "eng")
190 {
191 StrStrMap::const_iterator impTranslation;
192 impTranslation = m_mpLanguage.find ("Text~Error_Messages~ERROR_Missing_Translation");
193 if (impTranslation == m_mpLanguage.end())
194 {
195 m_mpLanguage["Text~Error_Messages~ERROR_Missing_Translation"] = "missing translation: %s";
196 }
197 m_bErrorMsgTranslationLoaded = true;
198 return 0;
199 }
200 if (ReadLanguagePackFooter (m_szLanguage) != 0)
201 {
202 return -1;
203 }
204
205 // Now - finally - let's get the translations.
206 if (ReadSingleTranslation (XNP_MAX_LANG_FILE_TEXT_ERROR_MSG, "ERROR_Missing_Translation", nullptr) == 0)
207 {
208 m_bErrorMsgTranslationLoaded = true;
209 }
210
211 for (StrStrMap::const_iterator impPosition = m_mpLanguage.begin();
212 impPosition != m_mpLanguage.end();
213 ++impPosition)
214 {
215 std::string strResult = ReadSingleTranslation (impPosition->first);
216 if (strResult != "")
217 {
218 m_mpLanguage[impPosition->first] = strResult;
219 }
220 else
221 {
222 // TODO: ?
223 }
224 }
225
226 Log.write (this->i18n ("Text~Error_Messages~INFO_Language_initialised"), cLog::eLOG_TYPE_INFO);
227
228 return 0;
229 }
230
CheckCurrentLanguagePack(bool bInsertMissingEntries)231 int cLanguage::CheckCurrentLanguagePack (bool bInsertMissingEntries)
232 {
233 //TODO: - JCK: Check and correct a language pack
234 return 0;
235 }
236
getAvailableLanguages() const237 std::vector<std::string> cLanguage::getAvailableLanguages() const
238 {
239 std::vector<std::string> languageCodes;
240
241 const auto fileNames = getFilesOfDirectory (LANGUAGE_FILE_FOLDER);
242
243 const auto languageFileNameSize = strlen (LANGUAGE_FILE_NAME);
244 const auto languageFileExtensionSize = strlen (LANGUAGE_FILE_EXT);
245
246 for (size_t i = 0; i < fileNames.size(); ++i)
247 {
248 const auto& fileName = fileNames[i];
249
250 if (fileName.size() <= languageFileNameSize + languageFileExtensionSize) continue;
251
252 const auto prefix = fileName.substr (0, languageFileNameSize);
253
254 if (prefix.compare (LANGUAGE_FILE_NAME) != 0) continue;
255
256 const auto suffix = fileName.substr (fileName.size() - languageFileExtensionSize);
257
258 if (suffix.compare (LANGUAGE_FILE_EXT) != 0) continue;
259
260 const auto languageCode = fileName.substr (languageFileNameSize, fileName.size() - languageFileExtensionSize - languageFileNameSize);
261
262 languageCodes.push_back (to_upper_copy (languageCode));
263 }
264
265 return languageCodes;
266 }
267
ReadSingleTranslation(const char * pszCurrent,...)268 int cLanguage::ReadSingleTranslation (const char* pszCurrent, ...)
269 {
270 va_list pvaArg;
271 va_start (pvaArg, pszCurrent);
272 StrStrMap::const_iterator impTranslation;
273 std::string szErrorMsg;
274
275 XMLElement* xmlElement = nullptr;
276 std::string szXmlNodePath;
277
278 for (;;)
279 {
280 xmlElement = m_XmlDoc.RootElement();
281 if (xmlElement == nullptr)
282 {
283 break;
284 }
285
286 if (strcmp (xmlElement->Value(), pszCurrent) != 0)
287 {
288 break;
289 }
290
291 do
292 {
293 pszCurrent = va_arg (pvaArg, char*);
294 if (pszCurrent != nullptr)
295 {
296 szXmlNodePath += "~";
297 szXmlNodePath += pszCurrent;
298 xmlElement = xmlElement->FirstChildElement (pszCurrent);
299 if (xmlElement == nullptr)
300 {
301 break;
302 }
303 }
304 }
305 while (pszCurrent != nullptr);
306 break;
307 }
308 szXmlNodePath.erase (0, 1);
309
310 if (xmlElement != nullptr)
311 {
312 const char* value = xmlElement->Attribute ("localized");
313 if (value)
314 {
315 va_end (pvaArg);
316 impTranslation = m_mpLanguage.find (szXmlNodePath);
317 if (impTranslation == m_mpLanguage.end())
318 {
319 szErrorMsg = "Language file: translation for >";
320 szErrorMsg += szXmlNodePath + "< is read more than once!";
321 Log.write (szErrorMsg, cLog::eLOG_TYPE_WARNING);
322 return -1;
323 }
324 m_mpLanguage[szXmlNodePath] = value;
325 return 0;
326 }
327 }
328
329 szErrorMsg = "Language file: translation for >";
330 if (xmlElement != nullptr)
331 {
332 const char* value = xmlElement->Attribute ("ENG");
333 if (value != nullptr)
334 {
335 m_mpLanguage[szXmlNodePath] = value;
336 szErrorMsg += std::string (value) + "< is missing";
337 }
338 else
339 {
340 if (m_bErrorMsgTranslationLoaded)
341 {
342 m_mpLanguage[szXmlNodePath] = i18n ("Text~Error_Messages~ERROR_Missing_Translation") + szXmlNodePath;
343 }
344 else
345 {
346 m_mpLanguage[szXmlNodePath] = std::string ("missing translation: ") + szXmlNodePath;
347 }
348 szErrorMsg += szXmlNodePath + "< is missing";
349 }
350 }
351 else
352 {
353 if (m_bErrorMsgTranslationLoaded)
354 {
355 m_mpLanguage[szXmlNodePath] = i18n ("Text~Error_Messages~ERROR_Missing_Translation", szXmlNodePath);
356 }
357 else
358 {
359 m_mpLanguage[szXmlNodePath] = std::string ("missing translation: ") + szXmlNodePath;
360 }
361 szErrorMsg += szXmlNodePath + "< is missing";
362 }
363 Log.write (szErrorMsg, cLog::eLOG_TYPE_WARNING);
364 va_end (pvaArg);
365 return -1;
366 }
367
368 /////////////
369 //ExTiXmlNode* node;
370 //node = XmlGetFirstNode (...);
371 //while (node != nullptr)
372 //{
373 // data = node->XmlReadNodeData (...);
374 // node = node-XmlGetFirstNodeChild();
375 //child = child->NextSibling() )
376 /////////////
377
ReadLanguagePackFooter()378 int cLanguage::ReadLanguagePackFooter()
379 {
380 return ReadLanguagePackFooter ("");
381 }
382
ReadLanguagePackFooter(const std::string & strLanguageCode)383 int cLanguage::ReadLanguagePackFooter (const std::string& strLanguageCode)
384 {
385 XMLElement* xmlElement = nullptr;
386 std::string strErrorMsg;
387 std::string strFileName;
388 std::string szLanguageCode (strLanguageCode);
389
390 if (szLanguageCode.empty())
391 {
392 strFileName = m_szLanguageFileMaster;
393 szLanguageCode = "eng";
394 }
395 else
396 {
397 strFileName = m_szLanguageFile;
398 }
399
400 // Load the file
401 if (m_XmlDoc.LoadFile (strFileName.c_str()) != XML_NO_ERROR)
402 {
403 strErrorMsg = "Can't open language file :" + strFileName;
404 Log.write (strErrorMsg, cLog::eLOG_TYPE_ERROR);
405 return -1;
406 }
407 // Is the main node correct ?
408 xmlElement = XmlGetFirstElement (m_XmlDoc, XNP_MAX_LANG_FILE);
409 if (xmlElement == nullptr)
410 {
411 strErrorMsg = "Language file (" + szLanguageCode + "): missing main node!";
412 Log.write (strErrorMsg, cLog::eLOG_TYPE_ERROR);
413 return -1;
414 }
415
416 // Who is responsible for the file ? (Who is to blame in case of errors?)
417 xmlElement = XmlGetFirstElement (m_XmlDoc, XNP_MAX_LANG_FILE_FOOTER_AUTHOR);
418 if (xmlElement == nullptr)
419 {
420 strErrorMsg = "Language file (" + szLanguageCode + "): missing author node!";
421 Log.write (strErrorMsg, cLog::eLOG_TYPE_ERROR);
422 }
423 else
424 {
425 //TODO: Find the last editor of the XML file
426 }
427
428 // Check the lang attribute of the main node
429 xmlElement = XmlGetFirstElement (m_XmlDoc, XNP_MAX_LANG_FILE);
430 const char* value = xmlElement->Attribute ("lang");
431 if (value == nullptr)
432 {
433 strErrorMsg = "Language file (" + szLanguageCode + "): language attribut missing! Language can not be identified";
434 Log.write (strErrorMsg, cLog::eLOG_TYPE_ERROR);
435 return -1;
436 }
437 else if (szLanguageCode != std::string (value))
438 {
439 strErrorMsg = "Language file (" + szLanguageCode + "): language attribut mismatch file name!";
440 Log.write (strErrorMsg, cLog::eLOG_TYPE_ERROR);
441 return -1;
442 }
443
444 // Writing is left-to-right or vice versa ?
445 value = xmlElement->Attribute ("direction");
446 if (value == nullptr)
447 {
448 strErrorMsg = "Language file (" + szLanguageCode + "): language attribut 'direction' is missing! Writing direction will be set to 'Left-To-Right'";
449 Log.write (strErrorMsg, cLog::eLOG_TYPE_WARNING);
450 m_bLeftToRight = true;
451 }
452 else if (std::string (value) == "left-to-right")
453 {
454 m_bLeftToRight = true;
455 }
456 else if (std::string (value) == "right-to-left")
457 {
458 m_bLeftToRight = false;
459 }
460 else
461 {
462 strErrorMsg = "Language file (" + szLanguageCode + "): language attribut 'direction' can not interpreted! Writing direction will be set to 'Left-To-Right'";
463 Log.write (strErrorMsg, cLog::eLOG_TYPE_WARNING);
464 m_bLeftToRight = true;
465 }
466
467 xmlElement = XmlGetFirstElement (m_XmlDoc, XNP_MAX_LANG_FILE_FOOTER_GAMEVERSION);
468 if (xmlElement == nullptr)
469 {
470 strErrorMsg = "Language file (" + szLanguageCode + "): missing game version node!";
471 Log.write (strErrorMsg, cLog::eLOG_TYPE_WARNING);
472 }
473 else
474 {
475 const char* value = xmlElement->Attribute ("time");
476 if (value == nullptr)
477 {
478 strErrorMsg = "Language file (" + szLanguageCode + "): game version attribute 'time' is missing!";
479 Log.write (strErrorMsg, cLog::eLOG_TYPE_WARNING);
480 }
481 else
482 {
483 int iTestResult = checkTimeStamp (value);
484 switch (iTestResult)
485 {
486 case -1 :
487 strErrorMsg = "Language file (" + szLanguageCode + "): game version attribute has wrong format!";
488 Log.write (strErrorMsg, cLog::eLOG_TYPE_WARNING);
489 break;
490 case 0 :
491 strErrorMsg = "Language file (" + szLanguageCode + "): may be outdated!";
492 Log.write (strErrorMsg, cLog::eLOG_TYPE_WARNING);
493 break;
494 case 1 :
495 strErrorMsg = "Language file (" + szLanguageCode + "): is newer than the game!";
496 Log.write (strErrorMsg, cLog::eLOG_TYPE_WARNING);
497 break;
498 case 2 :
499 // Timestamps match
500 break;
501 }
502 }
503 }
504 return 0;
505 }
506
507 // not const string& since it is modified
checkTimeStamp(std::string rstrData)508 int cLanguage::checkTimeStamp (std::string rstrData)
509 {
510 //JCK: Should be replaced by a faster and more secure function
511
512 // Index : 0123456789012345678
513 // Example : 2007-09-30 13:04:00
514
515 if (rstrData.length() != 19) return -1;
516 if (! isdigit (rstrData[0])) return -1;
517 if (! isdigit (rstrData[1])) return -1;
518 if (! isdigit (rstrData[2])) return -1;
519 if (! isdigit (rstrData[3])) return -1;
520 if (rstrData[4] != '-') return -1;
521 if (! isdigit (rstrData[5])) return -1;
522 if (! isdigit (rstrData[6])) return -1;
523 if (rstrData[7] != '-') return -1;
524 if (! isdigit (rstrData[8])) return -1;
525 if (! isdigit (rstrData[9])) return -1;
526 if (rstrData[10] != ' ') return -1;
527 if (! isdigit (rstrData[11])) return -1;
528 if (! isdigit (rstrData[12])) return -1;
529 if (rstrData[13] != ':') return -1;
530 if (! isdigit (rstrData[14])) return -1;
531 if (! isdigit (rstrData[15])) return -1;
532 if (rstrData[16] != ':') return -1;
533 if (! isdigit (rstrData[17])) return -1;
534 if (! isdigit (rstrData[18])) return -1;
535
536 std::string szTemp1 = rstrData;
537
538 szTemp1.erase (16, 1);
539 szTemp1.erase (13, 1);
540 szTemp1.erase (10, 1);
541 szTemp1.erase (7, 1);
542 szTemp1.erase (4, 1);
543
544 std::string szTemp2 = MAX_BUILD_DATE;
545
546 szTemp2.erase (16, 1);
547 szTemp2.erase (13, 1);
548 szTemp2.erase (10, 1);
549 szTemp2.erase (7, 1);
550 szTemp2.erase (4, 1);
551
552 if (szTemp1 < szTemp2)
553 {
554 return 0; // XML is older than the game
555 }
556 if (szTemp1 > szTemp2)
557 {
558 return 1; // XML is newer than the game
559 }
560 return 2; // XML is matching the game
561 }
562
ReadRecursiveLanguagePack(XMLElement * xmlElement,std::string strNodePath)563 int cLanguage::ReadRecursiveLanguagePack (XMLElement* xmlElement, std::string strNodePath)
564 {
565 if (xmlElement == nullptr)
566 {
567 return -1;
568 }
569
570 if (strNodePath[ strNodePath.length() - 1 ] != '~')
571 {
572 strNodePath += "~";
573 }
574
575 const char* value = xmlElement->Attribute ("ENG");
576 if (value != nullptr)
577 {
578 m_mpLanguage[strNodePath + xmlElement->Value()] = value;
579 Log.write (strNodePath + xmlElement->Value() + " : " + value, cLog::eLOG_TYPE_DEBUG);
580 }
581
582 XMLElement* xmlElementTMP = xmlElement->FirstChildElement();
583 if (xmlElementTMP != nullptr)
584 {
585 this->ReadRecursiveLanguagePack (xmlElementTMP, strNodePath + xmlElement->Value());
586 }
587
588 xmlElement = xmlElement->NextSiblingElement();
589 if (xmlElement != nullptr)
590 {
591 this->ReadRecursiveLanguagePack (xmlElement, strNodePath);
592 }
593
594 return 0;
595 }
596
ReadSingleTranslation(const std::string & strInput)597 std::string cLanguage::ReadSingleTranslation (const std::string& strInput)
598 {
599 if (strInput.empty()) return "";
600 const std::string& strPath = strInput;
601
602 XMLElement* xmlElement = m_XmlDoc.RootElement();
603 try
604 {
605 if (xmlElement == nullptr) throw std::string ("Can't excess root node");
606 if (strcmp (xmlElement->Value(), "MAX_Language_File") != 0) throw std::string ("Root node mismatch");
607 }
608 catch (const std::string& strMsg)
609 {
610 Log.write ("Language file (" + m_szLanguage + "): " + strMsg, cLog::eLOG_TYPE_WARNING);
611 return "";
612 }
613 std::size_t iPosBegin = 0;
614 try
615 {
616 do
617 {
618 std::size_t iPosEnd = strPath.find ('~', iPosBegin + 1);
619 std::string strCurrent = strPath.substr (iPosBegin, iPosEnd - iPosBegin);
620 if (strCurrent[0] == '~') strCurrent.erase (0, 1);
621 xmlElement = xmlElement->FirstChildElement (strCurrent.c_str());
622 if (xmlElement == nullptr)
623 throw i18n ("Text~Error_Messages~ERROR_Missing_Translation", m_mpLanguage[strInput]);
624
625 iPosBegin = iPosEnd;
626 }
627 while (iPosBegin != std::string::npos);
628 }
629 catch (const std::string& strMsg)
630 {
631 Log.write ("Language file (" + m_szLanguage + "): " + strMsg, cLog::eLOG_TYPE_WARNING);
632 return m_mpLanguage[strInput];
633 }
634
635 const char* translation = xmlElement->Attribute ("localized");
636 if (translation != nullptr)
637 {
638 return translation;
639 }
640
641 std::string szErrorMsg = "Language file: translation for >";
642 szErrorMsg += strInput + "< is missing";
643 Log.write (szErrorMsg, cLog::eLOG_TYPE_WARNING);
644
645 return "";
646 }
647