3 * =============================================================================
5 * Copyright (C) 2004-2007 AlliedModders LLC. All rights reserved.
6 * =============================================================================
8 * This program is free software; you can redistribute it and/or modify it under
9 * the terms of the GNU General Public License, version 3.0, as published by the
10 * Free Software Foundation.
12 * This program is distributed in the hope that it will be useful, but WITHOUT
13 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
14 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
17 * You should have received a copy of the GNU General Public License along with
18 * this program. If not, see <http://www.gnu.org/licenses/>.
20 * As a special exception, AlliedModders LLC gives you permission to link the
21 * code of this program (as well as its derivative works) to "Half-Life 2," the
22 * "Source Engine," the "SourcePawn JIT," and any Game MODs that run on software
23 * by the Valve Corporation. You must obey the GNU General Public License in
24 * all respects for all other code used. Additionally, AlliedModders LLC grants
25 * this exception to all derivative works. AlliedModders LLC defines further
26 * exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007),
27 * or <http://www.sourcemod.net/license.php>.
35 #include "Translator.h"
37 #include "TextParsers.h"
38 #include "LibrarySys.h"
39 #include "sm_stringutil.h"
40 #include "sourcemod.h"
41 #include "PlayerManager.h"
43 Translator g_Translator;
44 CPhraseFile *g_pCorePhrases = NULL;
55 unsigned int fmt_count;
56 unsigned int fmt_bytes;
58 unsigned int translations;
61 CPhraseFile::CPhraseFile(Translator *pTranslator, const char *file)
63 m_pStringTab = pTranslator->GetStringTable();
64 m_pMemory = m_pStringTab->GetMemTable();
65 m_LangCount = pTranslator->GetLanguageCount();
67 m_pTranslator = pTranslator;
68 m_pPhraseLookup = NULL;
71 CPhraseFile::~CPhraseFile()
75 sm_trie_destroy(m_pPhraseLookup);
79 void CPhraseFile::ParseError(const char *message, ...)
83 va_start(ap, message);
84 vsnprintf(buffer, sizeof(buffer), message, ap);
87 m_ParseError.assign(buffer);
90 void CPhraseFile::ParseWarning(const char *message, ...)
94 va_start(ap, message);
95 vsnprintf(buffer, sizeof(buffer), message, ap);
100 g_Logger.LogError("[SM] Warning(s) encountered in translation file \"%s\"", m_File.c_str());
104 g_Logger.LogError("[SM] %s", message);
107 void CPhraseFile::ReparseFile()
111 sm_trie_destroy(m_pPhraseLookup);
113 m_pPhraseLookup = sm_trie_create();
115 m_LangCount = m_pTranslator->GetLanguageCount();
123 char path[PLATFORM_MAX_PATH];
124 g_SourceMod.BuildPath(Path_SM, path, PLATFORM_MAX_PATH, "translations/%s", m_File.c_str());
126 //backwards compatibility shim
127 /* :HACKHACK: Change .cfg/.txt and vice versa for compatibility */
128 if (!g_LibSys.PathExists(path))
130 if (m_File.compare("common.cfg") == 0)
132 UTIL_ReplaceAll(path, sizeof(path), "common.cfg", "common.phrases.txt");
133 } else if (strstr(path, ".cfg")) {
134 UTIL_ReplaceAll(path, sizeof(path), ".cfg", ".txt");
135 } else if (strstr(path, ".txt")) {
136 UTIL_ReplaceAll(path, sizeof(path), ".txt", ".cfg");
140 unsigned int line=0, col=0;
142 if ((err=g_TextParser.ParseFile_SMC(path, this, &line, &col)) != SMCParse_Okay)
144 const char *msg = g_TextParser.GetSMCErrorString(err);
147 msg = m_ParseError.c_str();
150 g_Logger.LogError("[SM] Fatal error encountered parsing translation file \"%s\"", m_File.c_str());
151 g_Logger.LogError("[SM] Error (line %d, column %d): %s", line, col, msg);
155 void CPhraseFile::ReadSMC_ParseStart()
158 m_ParseState = PPS_None;
160 m_FileLogged = false;
161 m_LastPhraseString.clear();
164 SMCParseResult CPhraseFile::ReadSMC_RawLine(const char *line, unsigned int curline)
168 return SMCParse_Continue;
171 SMCParseResult CPhraseFile::ReadSMC_NewSection(const char *name, bool opt_quotes)
173 bool recognized = false;
174 if (m_ParseState == PPS_None)
176 if (strcmp(name, "Phrases") == 0)
178 m_ParseState = PPS_Phrases;
181 } else if (m_ParseState == PPS_Phrases) {
182 m_ParseState = PPS_InPhrase;
186 m_CurPhrase = m_pMemory->CreateMem(sizeof(phrase_t), (void **)&pPhrase);
188 /* Create the reverse lookup */
189 if (!sm_trie_insert(m_pPhraseLookup, name, reinterpret_cast<void *>(m_CurPhrase)))
191 ParseWarning("Skipping duplicate phrase \"%s\" on line %d.", name, m_CurLine);
192 /* :IDEA: prevent memory waste by seeking backwards in memtable? */
195 /* Initialize new phrase */
198 pPhrase->fmt_count = 0;
199 pPhrase->fmt_list = -1;
201 int trans_tbl = m_pMemory->CreateMem(sizeof(trans_t) * m_LangCount, (void **)&pTrans);
202 pPhrase = (phrase_t *)m_pMemory->GetAddress(m_CurPhrase);
203 pPhrase->trans_tbl = trans_tbl;
205 pPhrase->translations = 0;
206 pPhrase->fmt_bytes = 0;
208 for (unsigned int i=0; i<m_LangCount; i++)
210 pTrans[i].stridx = -1;
212 m_LastPhraseString.assign(name);
214 } else if (m_ParseState == PPS_InPhrase) {
215 ParseError("Phrase sections may not have sub-sections");
216 return SMCParse_HaltFail;
221 ParseWarning("Ignoring invalid section \"%s\" on line %d.", name, m_CurLine);
224 return SMCParse_Continue;
227 SMCParseResult CPhraseFile::ReadSMC_KeyValue(const char *key, const char *value, bool key_quotes, bool value_quotes)
229 /* See if we are ignoring a phrase */
230 if (m_CurPhrase == -1)
232 return SMCParse_Continue;
235 phrase_t *pPhrase = (phrase_t *)m_pMemory->GetAddress(m_CurPhrase);
237 if (key[0] == '#' && strcmp(key, "#format") == 0)
239 if (pPhrase->fmt_list != -1)
241 ParseWarning("Ignoring duplicated #format property on line %d", m_CurLine);
242 return SMCParse_Continue;
245 if (pPhrase->translations > 0)
247 ParseWarning("#format property should come before translations on line %d, ignoring", m_CurLine);
248 return SMCParse_Continue;
251 /* Do an initial browsing for error checking and what not */
259 ParseStates state = Parse_None;
261 const char *old_value = value;
262 const char *last_value = value;
263 while (*value != '\0')
265 if (state == Parse_None)
269 pPhrase->fmt_count++;
271 } else if (*value == ',') {
274 unsigned int bytes = g_TextParser.GetUTF8CharBytes(value);
275 if (bytes != 1 || !isalpha(*value))
277 ParseWarning("Invalid token '%c' in #format property on line %d.", *value, m_CurLine);
280 } else if (state == Parse_Index) {
283 state = Parse_Format;
284 if (value - last_value >= 15)
286 ParseWarning("Too many digits in format index on line %d, phrase will be ignored.", m_CurLine);
288 return SMCParse_Continue;
291 unsigned int bytes = g_TextParser.GetUTF8CharBytes(value);
292 if (bytes != 1 || !isdigit(*value))
294 ParseWarning("Token '%c' in #format property on line %d is not a digit, phrase will be ignored.",
298 return SMCParse_Continue;
301 } else if (state == Parse_Format) {
305 last_value = value + 1;
311 if (state != Parse_None)
313 /* Moose clam cow. */
314 ParseWarning("Unterminated format string on line %d, phrase will be ignored.", m_CurLine);
316 return SMCParse_Continue;
320 /* Allocate the format table */
323 int tmp = m_pMemory->CreateMem(sizeof(int) * pPhrase->fmt_count, (void **)&fmt_list);
325 /* Update the phrase pointer in case it changed */
326 pPhrase = (phrase_t *)m_pMemory->GetAddress(m_CurPhrase);
327 pPhrase->fmt_list = tmp;
330 for (size_t i=0; i<pPhrase->fmt_count; i++)
335 /* Now we need to read again... this time into the format buffer */
336 const char *in_ptr = value;
337 const char *idx_ptr = NULL;
338 char *out_ptr = NULL;
339 unsigned int cur_idx = 0;
341 while (*in_ptr != '\0')
343 if (state == Parse_None)
350 } else if (state == Parse_Index) {
353 /* Check the number! */
356 ParseWarning("Format property contains unindexed format string on line %d, phrase will be ignored.", m_CurLine);
358 return SMCParse_Continue;
360 long idx = strtol(idx_ptr, NULL, 10);
361 if (idx < 1 || idx > (long)pPhrase->fmt_count)
363 ParseWarning("Format property contains invalid index '%d' on line %d, phrase will be ignored.", idx, m_CurLine);
365 return SMCParse_Continue;
366 } else if (fmt_list[idx - 1] != -1) {
367 ParseWarning("Format property contains duplicated index '%d' on line %d, phrase will be ignored.", idx, m_CurLine);
369 return SMCParse_Continue;
371 cur_idx = (unsigned int)idx;
372 state = Parse_Format;
374 } else if (!idx_ptr) {
377 } else if (state == Parse_Format) {
382 ParseWarning("Format property contains empty format string on line %d, phrase will be ignored.", m_CurLine);
384 return SMCParse_Continue;
388 /* Now, add this to our table */
389 int tmp_idx = m_pStringTab->AddString(fmt_buf);
390 /* Update pointers and update necessary variables */
391 pPhrase = (phrase_t *)m_pMemory->GetAddress(m_CurPhrase);
392 pPhrase->fmt_bytes += strlen(fmt_buf);
393 fmt_list = (int *)m_pMemory->GetAddress(pPhrase->fmt_list);
394 fmt_list[cur_idx - 1] = tmp_idx;
401 /* Check length ... */
402 if ((unsigned)(out_ptr - fmt_buf) >= sizeof(fmt_buf) - 1)
404 ParseWarning("Format property contains format string that exceeds maximum length on line %d, phrase will be ignored.",
407 return SMCParse_Continue;
409 *out_ptr++ = *in_ptr;
415 /* If we've gotten here, we only accepted unique indexes in a range.
416 * Therefore, the list has to be completely filled. Double check anyway.
418 for (size_t i=0; i<pPhrase->fmt_count; i++)
420 if (fmt_list[i] == -1)
422 ParseWarning("Format property contains no string for index %d on line %d, phrase will be ignored.",
426 return SMCParse_Continue;
430 size_t len = strlen(key);
433 ParseWarning("Ignoring translation to invalid language \"%s\" on line %d.", key, m_CurLine);
434 return SMCParse_Continue;
438 if (!m_pTranslator->GetLanguageByCode(key, &lang))
440 /* Ignore if we don't have a language.
441 * :IDEA: issue a one-time alert?
443 return SMCParse_Continue;
446 /* See how many bytes we need for this string, then allocate.
447 * NOTE: THIS SHOULD GUARANTEE THAT WE DO NOT NEED TO NEED TO SIZE CHECK
449 len = strlen(value) + pPhrase->fmt_bytes + 1;
453 out_idx = m_pMemory->CreateMem(len, (void **)&out_buf);
456 pPhrase = (phrase_t *)m_pMemory->GetAddress(m_CurPhrase);
459 int *fmt_list = (int *)m_pMemory->GetAddress(pPhrase->fmt_list);
460 trans_t *pTrans = (trans_t *)m_pMemory->GetAddress(pPhrase->trans_tbl);
462 pTrans = &pTrans[lang];
463 pTrans->stridx = out_idx;
465 bool params[MAX_TRANSLATE_PARAMS];
467 /* Build the format order list, if necessary */
470 int tmp = m_pMemory->CreateMem(pPhrase->fmt_count * sizeof(int), (void **)&fmt_order);
472 /* Update pointers */
473 pPhrase = (phrase_t *)m_pMemory->GetAddress(m_CurPhrase);
474 pTrans = (trans_t *)m_pMemory->GetAddress(pPhrase->trans_tbl);
475 fmt_list = (int *)m_pMemory->GetAddress(pPhrase->fmt_list);
476 pTrans = &pTrans[lang];
478 /* Now it's safe to save the index */
479 pTrans->fmt_order = tmp;
481 for (unsigned int i=0; i<pPhrase->fmt_count; i++)
485 memset(¶ms[0], 0, sizeof(params));
488 const char *in_ptr = value;
489 char *out_ptr = out_buf;
490 unsigned int order_idx = 0;
491 while (*in_ptr != '\0')
495 switch (*(in_ptr + 1))
510 /* Copy both bytes since we don't know what's going on */
511 *out_ptr++ = *in_ptr++;
512 *out_ptr++ = *in_ptr;
515 /* Skip past the last byte read */
517 } else if (*in_ptr == '{' && fmt_list != NULL) {
518 /* Search for parameters if this is a formatted string */
519 const char *scrap_in_point = in_ptr;
520 const char *digit_start = ++in_ptr;
522 while (*in_ptr != '\0')
524 bytes = g_TextParser.GetUTF8CharBytes(in_ptr);
531 /* Did we get an index? */
532 if (in_ptr == digit_start)
537 long idx = strtol(digit_start, NULL, 10);
538 if (idx < 1 || idx > (int)pPhrase->fmt_count)
547 /* We're safe to insert the string. First mark order. */
548 fmt_order[order_idx++] = (int)idx - 1;
549 /* Now concatenate */
550 out_ptr += sprintf(out_ptr, "%s", m_pStringTab->GetString(fmt_list[idx-1]));
552 params[idx-1] = true;
559 /* Pretend none of this ever happened. Move along now! */
560 in_ptr = scrap_in_point;
561 *out_ptr++ = *in_ptr;
563 *out_ptr++ = *in_ptr;
569 pPhrase->translations++;
572 return SMCParse_Continue;
575 SMCParseResult CPhraseFile::ReadSMC_LeavingSection()
577 if (m_ParseState == PPS_InPhrase)
579 if (m_CurPhrase == -1 && m_LastPhraseString.size())
581 sm_trie_delete(m_pPhraseLookup, m_LastPhraseString.c_str());
584 m_ParseState = PPS_Phrases;
585 m_LastPhraseString.assign("");
586 } else if (m_ParseState == PPS_Phrases) {
587 m_ParseState = PPS_None;
590 return SMCParse_Continue;
593 void CPhraseFile::ReadSMC_ParseEnd(bool halted, bool failed)
595 /* Check to see if we have any dangling phrases that weren't completed, and scrap them */
596 if ((halted || failed) && m_LastPhraseString.size())
598 sm_trie_delete(m_pPhraseLookup, m_LastPhraseString.c_str());
602 TransError CPhraseFile::GetTranslation(const char *szPhrase, unsigned int lang_id, Translation *pTrans)
604 if (lang_id >= m_LangCount)
606 return Trans_BadLanguage;
610 if (!sm_trie_retrieve(m_pPhraseLookup, szPhrase, &object))
612 return Trans_BadPhrase;
615 phrase_t *pPhrase = (phrase_t *)m_pMemory->GetAddress(reinterpret_cast<int>(object));
616 trans_t *trans = (trans_t *)m_pMemory->GetAddress(pPhrase->trans_tbl);
618 trans = &trans[lang_id];
620 if (trans->stridx == -1)
622 return Trans_BadPhraseLanguage;
625 pTrans->fmt_order = (int *)m_pMemory->GetAddress(trans->fmt_order);
626 pTrans->fmt_count = pPhrase->fmt_count;
628 pTrans->szPhrase = m_pStringTab->GetString(trans->stridx);
633 const char *CPhraseFile::GetFilename()
635 return m_File.c_str();
638 /**************************
639 ** MAIN TRANSLATOR CODE **
640 **************************/
642 Translator::Translator() : m_ServerLang(LANGUAGE_ENGLISH)
644 m_pStringTab = new BaseStringTable(2048);
645 m_pLCodeLookup = sm_trie_create();
646 strncopy(m_InitialLang, "en", sizeof(m_InitialLang));
649 Translator::~Translator()
651 for (size_t i=0; i<m_Files.size(); i++)
656 for (size_t i=0; i<m_Languages.size(); i++)
658 delete m_Languages[i];
661 sm_trie_destroy(m_pLCodeLookup);
666 ConfigResult Translator::OnSourceModConfigChanged(const char *key,
672 if (strcasecmp(key, "ServerLang") == 0)
674 if (source == ConfigSource_Console)
677 if (!GetLanguageByCode(value, &index))
679 UTIL_Format(error, maxlength, "Language code \"%s\" is not registered", value);
680 return ConfigResult_Reject;
683 m_ServerLang = index;
685 strncopy(m_InitialLang, value, sizeof(m_InitialLang));
688 return ConfigResult_Accept;
691 return ConfigResult_Ignore;
694 void Translator::OnSourceModLevelChange(const char *mapName)
696 /* Refresh language stuff */
697 char path[PLATFORM_MAX_PATH];
698 g_SourceMod.BuildPath(Path_SM, path, sizeof(path), "configs/languages.cfg");
699 RebuildLanguageDatabase(path);
702 void Translator::OnSourceModAllInitialized()
704 AddLanguage("en", "English");
708 //backwards compatibility shim
709 /* hack -- if core.cfg exists, exec it instead. */
710 char path[PLATFORM_MAX_PATH];
711 g_SourceMod.BuildPath(Path_SM, path, sizeof(path), "translations/core.cfg");
713 if (g_LibSys.PathExists(path))
715 id = FindOrAddPhraseFile("core.cfg");
717 id = FindOrAddPhraseFile("core.phrases.txt");
720 g_pCorePhrases = GetFileByIndex(id);
723 bool Translator::GetLanguageByCode(const char *code, unsigned int *index)
727 if (!sm_trie_retrieve(m_pLCodeLookup, code, &_index))
734 *index = reinterpret_cast<unsigned int>(_index);
740 bool Translator::GetLanguageByName(const char *name, unsigned int *index)
742 CVector<Language *>::iterator iter;
745 for (iter=m_Languages.begin(); iter!=m_Languages.end(); iter++, id++)
747 if (strcasecmp(m_pStringTab->GetString((*iter)->m_FullName), name) == 0)
753 if (iter == m_Languages.end())
766 unsigned int Translator::GetLanguageCount()
768 return (unsigned int)m_Languages.size();
771 BaseStringTable *Translator::GetStringTable()
776 unsigned int Translator::FindOrAddPhraseFile(const char *phrase_file)
778 for (size_t i=0; i<m_Files.size(); i++)
780 if (strcmp(m_Files[i]->GetFilename(), phrase_file) == 0)
782 return (unsigned int)i;
786 CPhraseFile *pFile = new CPhraseFile(this, phrase_file);
787 unsigned int idx = (unsigned int)m_Files.size();
789 m_Files.push_back(pFile);
791 pFile->ReparseFile();
796 void Translator::RebuildLanguageDatabase(const char *lang_header_file)
798 /* Erase everything we have */
799 sm_trie_destroy(m_pLCodeLookup);
800 m_pLCodeLookup = sm_trie_create();
801 m_pStringTab->Reset();
803 for (size_t i=0; i<m_Languages.size(); i++)
805 delete m_Languages[i];
811 unsigned int line=0, col=0;
812 if ((err=g_TextParser.ParseFile_SMC(lang_header_file, this, &line, &col)) != SMCParse_Okay)
814 const char *str_err = g_TextParser.GetSMCErrorString(err);
817 str_err = m_CustomError.c_str();
820 g_Logger.LogError("[SM] Failed to parse language header file: \"%s\"", lang_header_file);
821 g_Logger.LogError("[SM] Parse error (line %d, column %d): %s", line, col, str_err);
826 if (!sm_trie_retrieve(m_pLCodeLookup, m_InitialLang, &serverLang))
828 g_Logger.LogError("Server language was set to bad language \"%s\" -- reverting to English", m_InitialLang);
830 strncopy(m_InitialLang, "en", sizeof(m_InitialLang));
831 m_ServerLang = LANGUAGE_ENGLISH;
834 m_ServerLang = reinterpret_cast<unsigned int>(serverLang);
836 if (!m_Languages.size())
838 g_Logger.LogError("[SM] Fatal error, no languages found! Translation will not work.");
841 for (size_t i=0; i<m_Files.size(); i++)
843 m_Files[i]->ReparseFile();
847 void Translator::ReadSMC_ParseStart()
849 m_InLanguageSection = false;
850 m_CustomError.clear();
853 SMCParseResult Translator::ReadSMC_NewSection(const char *name, bool opt_quotes)
855 if (!m_InLanguageSection)
857 if (strcmp(name, "Languages") == 0)
859 m_InLanguageSection = true;
863 if (!m_InLanguageSection)
865 g_Logger.LogError("[SM] Warning: Unrecognized section \"%s\" in languages.cfg", name);
868 return SMCParse_Continue;
871 SMCParseResult Translator::ReadSMC_LeavingSection()
873 m_InLanguageSection = false;
875 return SMCParse_Continue;
878 SMCParseResult Translator::ReadSMC_KeyValue(const char *key, const char *value, bool key_quotes, bool value_quotes)
880 size_t len = strlen(key);
884 g_Logger.LogError("[SM] Warning encountered parsing languages.cfg file.");
885 g_Logger.LogError("[SM] Invalid language code \"%s\" is being ignored.", key);
888 AddLanguage(key, value);
890 return SMCParse_Continue;
893 bool Translator::AddLanguage(const char *langcode, const char *description)
895 if (sm_trie_retrieve(m_pLCodeLookup, langcode, NULL))
900 Language *pLanguage = new Language;
901 unsigned int idx = m_Languages.size();
903 pLanguage->m_code2[0] = langcode[0];
904 pLanguage->m_code2[1] = langcode[1];
905 pLanguage->m_code2[2] = langcode[2];
906 pLanguage->m_FullName = m_pStringTab->AddString(description);
908 sm_trie_insert(m_pLCodeLookup, langcode, reinterpret_cast<void *>(idx));
910 m_Languages.push_back(pLanguage);
915 CPhraseFile *Translator::GetFileByIndex(unsigned int index)
917 if (index >= m_Files.size())
922 return m_Files[index];
925 size_t Translator::Translate(char *buffer, size_t maxlength, void **params, const Translation *pTrans)
927 void *new_params[MAX_TRANSLATE_PARAMS];
929 /* Rewrite the parameter order */
930 for (unsigned int i=0; i<pTrans->fmt_count; i++)
932 new_params[i] = params[pTrans->fmt_order[i]];
935 return gnprintf(buffer, maxlength, pTrans->szPhrase, new_params);
938 TransError Translator::CoreTrans(int client,
945 /* :TODO: do language stuff here */
949 return Trans_BadPhraseFile;
955 /* Using server lang temporarily until client lang stuff is implemented */
956 if ((err=g_pCorePhrases->GetTranslation(phrase, m_ServerLang, &trans)) != Trans_Okay)
961 size_t len = Translate(buffer, maxlength, params, &trans);
971 unsigned int Translator::GetServerLanguage()
976 unsigned int Translator::GetClientLanguage(int client)
978 CPlayer *pPlayer = g_Players.GetPlayerByIndex(client);
979 return pPlayer->GetLanguageId();
982 bool Translator::GetLanguageInfo(unsigned int number, const char **code, const char **name)
984 if (number >= GetLanguageCount())
989 Language *l = m_Languages[number];
996 *name = m_pStringTab->GetString(l->m_FullName);