3 * =============================================================================
5 * Copyright (C) 2004-2008 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>.
32 #include "common_logic.h"
36 #include <sm_platform.h>
37 #include "Translator.h"
38 #include <IPlayerHelpers.h>
39 #include <ISourceMod.h>
40 #include <ILibrarySys.h>
41 #include "PhraseCollection.h"
42 #include "stringutil.h"
44 Translator g_Translator;
45 IPhraseCollection *g_pCorePhrases = NULL;
46 unsigned int g_pCorePhraseID = 0;
48 using namespace SourceMod;
59 unsigned int fmt_count;
60 unsigned int fmt_bytes;
62 unsigned int translations;
65 CPhraseFile::CPhraseFile(Translator *pTranslator, const char *file)
67 m_pStringTab = pTranslator->GetStringTable();
68 m_pMemory = m_pStringTab->GetMemTable();
69 m_LangCount = pTranslator->GetLanguageCount();
71 m_pTranslator = pTranslator;
74 CPhraseFile::~CPhraseFile()
78 void CPhraseFile::ParseError(const char *message, ...)
82 va_start(ap, message);
83 vsnprintf(buffer, sizeof(buffer), message, ap);
86 m_ParseError.assign(buffer);
89 void CPhraseFile::ParseWarning(const char *message, ...)
93 va_start(ap, message);
94 vsnprintf(buffer, sizeof(buffer), message, ap);
99 smcore.LogError("[SM] Warning(s) encountered in translation file \"%s\"", m_File.c_str());
103 smcore.LogError("[SM] %s", buffer);
106 void CPhraseFile::ReparseFile()
108 m_PhraseLookup.clear();
110 m_LangCount = m_pTranslator->GetLanguageCount();
118 char path[PLATFORM_MAX_PATH];
119 g_pSM->BuildPath(Path_SM, path, PLATFORM_MAX_PATH, "translations/%s", m_File.c_str());
121 //backwards compatibility shim
122 /* :HACKHACK: Change .cfg/.txt and vice versa for compatibility */
123 if (!libsys->PathExists(path))
125 if (m_File.compare("common.cfg") == 0)
127 UTIL_ReplaceAll(path, sizeof(path), "common.cfg", "common.phrases.txt", true);
128 } else if (strstr(path, ".cfg")) {
129 UTIL_ReplaceAll(path, sizeof(path), ".cfg", ".txt", true);
130 } else if (strstr(path, ".txt")) {
131 UTIL_ReplaceAll(path, sizeof(path), ".txt", ".cfg", true);
137 if ((err=textparsers->ParseFile_SMC(path, this, &states)) != SMCError_Okay)
139 const char *msg = textparsers->GetSMCErrorString(err);
142 msg = m_ParseError.c_str();
145 smcore.LogError("[SM] Fatal error encountered parsing translation file \"%s\"", m_File.c_str());
146 smcore.LogError("[SM] Error (line %d, column %d): %s", states.line, states.col, msg);
150 for (unsigned int i = 1; i < m_LangCount; i++)
152 if (!m_pTranslator->GetLanguageInfo(i, &code, NULL))
157 g_pSM->BuildPath(Path_SM,
160 "translations/%s/%s",
164 /* Speculatively load these. */
165 if (!libsys->PathExists(path))
170 if ((err=textparsers->ParseFile_SMC(path, this, &states)) != SMCError_Okay)
172 const char *msg = textparsers->GetSMCErrorString(err);
175 msg = m_ParseError.c_str();
178 smcore.LogError("[SM] Fatal error encountered parsing translation file \"%s/%s\"",
181 smcore.LogError("[SM] Error (line %d, column %d): %s",
189 void CPhraseFile::ReadSMC_ParseStart()
192 m_ParseState = PPS_None;
193 m_FileLogged = false;
194 m_LastPhraseString.clear();
197 SMCResult CPhraseFile::ReadSMC_NewSection(const SMCStates *states, const char *name)
199 bool recognized = false;
200 if (m_ParseState == PPS_None)
202 if (strcmp(name, "Phrases") == 0)
204 m_ParseState = PPS_Phrases;
208 else if (m_ParseState == PPS_Phrases)
210 m_ParseState = PPS_InPhrase;
214 if ((pvalue = m_PhraseLookup.retrieve(name)) != NULL)
216 m_CurPhrase = *pvalue;
222 m_CurPhrase = m_pMemory->CreateMem(sizeof(phrase_t), (void **)&pPhrase);
223 m_PhraseLookup.insert(name, m_CurPhrase);
225 /* Initialize new phrase */
228 pPhrase->fmt_count = 0;
229 pPhrase->fmt_list = -1;
231 int trans_tbl = m_pMemory->CreateMem(sizeof(trans_t) * m_LangCount, (void **)&pTrans);
232 pPhrase = (phrase_t *)m_pMemory->GetAddress(m_CurPhrase);
233 pPhrase->trans_tbl = trans_tbl;
235 pPhrase->translations = 0;
236 pPhrase->fmt_bytes = 0;
238 for (unsigned int i=0; i<m_LangCount; i++)
240 pTrans[i].stridx = -1;
244 m_LastPhraseString.assign(name);
246 else if (m_ParseState == PPS_InPhrase)
248 ParseError("Phrase sections may not have sub-sections");
249 return SMCResult_HaltFail;
254 ParseWarning("Ignoring invalid section \"%s\" on line %d.", name, states->line);
257 return SMCResult_Continue;
260 SMCResult CPhraseFile::ReadSMC_KeyValue(const SMCStates *states, const char *key, const char *value)
262 /* See if we are ignoring a phrase */
263 if (m_CurPhrase == -1)
265 return SMCResult_Continue;
268 phrase_t *pPhrase = (phrase_t *)m_pMemory->GetAddress(m_CurPhrase);
270 /* Duplicate format keys get silently ignored. */
272 && strcmp(key, "#format") == 0
273 && pPhrase->fmt_list == -1)
275 if (pPhrase->translations > 0)
277 ParseWarning("#format property should come before translations on line %d, ignoring", states->line);
278 return SMCResult_Continue;
281 /* Do an initial browsing for error checking and what not */
289 ParseStates state = Parse_None;
291 const char *old_value = value;
292 const char *last_value = value;
293 while (*value != '\0')
295 if (state == Parse_None)
299 pPhrase->fmt_count++;
302 else if (*value == ',')
308 unsigned int bytes = textparsers->GetUTF8CharBytes(value);
309 if (bytes != 1 || !isalpha(*value))
311 ParseWarning("Invalid token '%c' in #format property on line %d.", *value, states->line);
315 else if (state == Parse_Index)
319 state = Parse_Format;
320 if (value - last_value >= 15)
322 ParseWarning("Too many digits in format index on line %d, phrase will be ignored.", states->line);
324 return SMCResult_Continue;
329 unsigned int bytes = textparsers->GetUTF8CharBytes(value);
330 if (bytes != 1 || !isdigit(*value))
332 ParseWarning("Token '%c' in #format property on line %d is not a digit, phrase will be ignored.",
336 return SMCResult_Continue;
340 else if (state == Parse_Format)
345 last_value = value + 1;
351 if (state != Parse_None)
353 /* Moose clam cow. */
354 ParseWarning("Unterminated format string on line %d, phrase will be ignored.", states->line);
356 return SMCResult_Continue;
360 /* Allocate the format table */
363 int tmp = m_pMemory->CreateMem(sizeof(int) * pPhrase->fmt_count, (void **)&fmt_list);
365 /* Update the phrase pointer in case it changed */
366 pPhrase = (phrase_t *)m_pMemory->GetAddress(m_CurPhrase);
367 pPhrase->fmt_list = tmp;
370 for (size_t i=0; i<pPhrase->fmt_count; i++)
375 /* Now we need to read again... this time into the format buffer */
376 const char *in_ptr = value;
377 const char *idx_ptr = NULL;
378 char *out_ptr = NULL;
379 unsigned int cur_idx = 0;
381 while (*in_ptr != '\0')
383 if (state == Parse_None)
391 else if (state == Parse_Index)
395 /* Check the number! */
398 ParseWarning("Format property contains unindexed format string on line %d, phrase will be ignored.", states->line);
400 return SMCResult_Continue;
402 long idx = strtol(idx_ptr, NULL, 10);
403 if (idx < 1 || idx > (long)pPhrase->fmt_count)
405 ParseWarning("Format property contains invalid index '%d' on line %d, phrase will be ignored.", idx, states->line);
407 return SMCResult_Continue;
409 else if (fmt_list[idx - 1] != -1)
411 ParseWarning("Format property contains duplicated index '%d' on line %d, phrase will be ignored.", idx, states->line);
413 return SMCResult_Continue;
415 cur_idx = (unsigned int)idx;
416 state = Parse_Format;
424 else if (state == Parse_Format)
430 ParseWarning("Format property contains empty format string on line %d, phrase will be ignored.", states->line);
432 return SMCResult_Continue;
436 /* Now, add this to our table */
437 int tmp_idx = m_pStringTab->AddString(fmt_buf);
438 /* Update pointers and update necessary variables */
439 pPhrase = (phrase_t *)m_pMemory->GetAddress(m_CurPhrase);
440 pPhrase->fmt_bytes += strlen(fmt_buf);
441 fmt_list = (int *)m_pMemory->GetAddress(pPhrase->fmt_list);
442 fmt_list[cur_idx - 1] = tmp_idx;
451 /* Check length ... */
452 if ((unsigned)(out_ptr - fmt_buf) >= sizeof(fmt_buf) - 1)
454 ParseWarning("Format property contains format string that exceeds maximum length on line %d, phrase will be ignored.",
457 return SMCResult_Continue;
459 *out_ptr++ = *in_ptr;
465 /* If we've gotten here, we only accepted unique indexes in a range.
466 * Therefore, the list has to be completely filled. Double check anyway.
468 for (size_t i=0; i<pPhrase->fmt_count; i++)
470 if (fmt_list[i] == -1)
472 ParseWarning("Format property contains no string for index %d on line %d, phrase will be ignored.",
476 return SMCResult_Continue;
482 size_t len = strlen(key);
483 if (len < 2 || len > 3)
485 ParseWarning("Ignoring translation to invalid language \"%s\" on line %d.", key, states->line);
486 return SMCResult_Continue;
490 if (!m_pTranslator->GetLanguageByCode(key, &lang))
492 /* Ignore if we don't have a language.
493 * :IDEA: issue a one-time alert?
495 return SMCResult_Continue;
498 /* See how many bytes we need for this string, then allocate.
499 * NOTE: THIS SHOULD GUARANTEE THAT WE DO NOT NEED TO NEED TO SIZE CHECK
501 len = strlen(value) + pPhrase->fmt_bytes + 1;
505 out_idx = m_pMemory->CreateMem(len, (void **)&out_buf);
508 pPhrase = (phrase_t *)m_pMemory->GetAddress(m_CurPhrase);
511 int *fmt_list = (int *)m_pMemory->GetAddress(pPhrase->fmt_list);
512 trans_t *pTrans = (trans_t *)m_pMemory->GetAddress(pPhrase->trans_tbl);
514 pTrans = &pTrans[lang];
515 pTrans->stridx = out_idx;
517 bool params[MAX_TRANSLATE_PARAMS];
519 /* Build the format order list, if necessary */
522 int tmp = m_pMemory->CreateMem(pPhrase->fmt_count * sizeof(int), (void **)&fmt_order);
524 /* Update pointers */
525 pPhrase = (phrase_t *)m_pMemory->GetAddress(m_CurPhrase);
526 pTrans = (trans_t *)m_pMemory->GetAddress(pPhrase->trans_tbl);
527 fmt_list = (int *)m_pMemory->GetAddress(pPhrase->fmt_list);
528 out_buf = (char *)m_pMemory->GetAddress(out_idx);
529 pTrans = &pTrans[lang];
531 /* Now it's safe to save the index */
532 pTrans->fmt_order = tmp;
534 for (unsigned int i=0; i<pPhrase->fmt_count; i++)
538 memset(¶ms[0], 0, sizeof(params));
541 const char *in_ptr = value;
542 char *out_ptr = out_buf;
543 unsigned int order_idx = 0;
544 while (*in_ptr != '\0')
548 switch (*(in_ptr + 1))
563 /* Copy both bytes since we don't know what's going on */
564 *out_ptr++ = *in_ptr++;
565 *out_ptr++ = *in_ptr;
568 /* Skip past the last byte read */
571 else if (*in_ptr == '{' && fmt_list != NULL)
573 /* Search for parameters if this is a formatted string */
574 const char *scrap_in_point = in_ptr;
575 const char *digit_start = ++in_ptr;
577 while (*in_ptr != '\0')
579 bytes = textparsers->GetUTF8CharBytes(in_ptr);
586 /* Did we get an index? */
587 if (in_ptr == digit_start)
592 long idx = strtol(digit_start, NULL, 10);
593 if (idx < 1 || idx > (int)pPhrase->fmt_count)
602 /* We're safe to insert the string. First mark order. */
603 fmt_order[order_idx++] = (int)idx - 1;
604 /* Now concatenate */
605 out_ptr += sprintf(out_ptr, "%s", m_pStringTab->GetString(fmt_list[idx-1]));
607 params[idx-1] = true;
614 /* Pretend none of this ever happened. Move along now! */
615 in_ptr = scrap_in_point;
616 *out_ptr++ = *in_ptr;
618 *out_ptr++ = *in_ptr;
624 pPhrase->translations++;
627 return SMCResult_Continue;
630 SMCResult CPhraseFile::ReadSMC_LeavingSection(const SMCStates *states)
632 if (m_ParseState == PPS_InPhrase)
634 if (m_CurPhrase == -1 && m_LastPhraseString.size())
635 m_PhraseLookup.remove(m_LastPhraseString.c_str());
637 m_ParseState = PPS_Phrases;
638 m_LastPhraseString.assign("");
640 else if (m_ParseState == PPS_Phrases)
642 m_ParseState = PPS_None;
645 return SMCResult_Continue;
648 void CPhraseFile::ReadSMC_ParseEnd(bool halted, bool failed)
650 /* Check to see if we have any dangling phrases that weren't completed, and scrap them */
651 if ((halted || failed) && m_LastPhraseString.size())
652 m_PhraseLookup.remove(m_LastPhraseString.c_str());
655 TransError CPhraseFile::GetTranslation(const char *szPhrase, unsigned int lang_id, Translation *pTrans)
657 if (lang_id >= m_LangCount)
659 return Trans_BadLanguage;
663 if ((pvalue = m_PhraseLookup.retrieve(szPhrase)) == NULL)
664 return Trans_BadPhrase;
666 phrase_t *pPhrase = (phrase_t *)m_pMemory->GetAddress(*pvalue);
667 trans_t *trans = (trans_t *)m_pMemory->GetAddress(pPhrase->trans_tbl);
669 trans = &trans[lang_id];
671 if (trans->stridx == -1)
673 return Trans_BadPhraseLanguage;
676 pTrans->fmt_count = pPhrase->fmt_count;
677 pTrans->fmt_order = pTrans->fmt_count > 0 ? (int *)m_pMemory->GetAddress(trans->fmt_order) : NULL;
678 pTrans->szPhrase = m_pStringTab->GetString(trans->stridx);
683 const char *CPhraseFile::GetFilename()
685 return m_File.c_str();
688 /**************************
689 ** MAIN TRANSLATOR CODE **
690 **************************/
692 Translator::Translator() : m_ServerLang(SOURCEMOD_LANGUAGE_ENGLISH)
694 m_pStringTab = new BaseStringTable(2048);
695 strncopy(m_InitialLang, "en", sizeof(m_InitialLang));
698 Translator::~Translator()
700 for (size_t i=0; i<m_Files.size(); i++)
705 for (size_t i=0; i<m_Languages.size(); i++)
707 delete m_Languages[i];
713 ConfigResult Translator::OnSourceModConfigChanged(const char *key,
719 if (strcasecmp(key, "ServerLang") == 0)
721 if (source == ConfigSource_Console)
724 if (!GetLanguageByCode(value, &index))
726 smcore.Format(error, maxlength, "Language code \"%s\" is not registered", value);
727 return ConfigResult_Reject;
730 m_ServerLang = index;
732 smcore.strncopy(m_InitialLang, value, sizeof(m_InitialLang));
735 return ConfigResult_Accept;
738 return ConfigResult_Ignore;
741 void Translator::OnSourceModLevelChange(const char *mapName)
743 /* Refresh language stuff */
744 char path[PLATFORM_MAX_PATH];
745 g_pSM->BuildPath(Path_SM, path, sizeof(path), "configs/languages.cfg");
746 RebuildLanguageDatabase(path);
749 void Translator::OnSourceModAllInitialized()
751 AddLanguage("en", "English");
753 g_pCorePhrases = CreatePhraseCollection();
754 g_pCorePhrases->AddPhraseFile("core.phrases");
756 sharesys->AddInterface(NULL, this);
759 void Translator::OnSourceModShutdown()
761 g_pCorePhrases->Destroy();
764 bool Translator::GetLanguageByCode(const char *code, unsigned int *index)
766 unsigned int *pindex;
768 if ((pindex = m_LCodeLookup.retrieve(code)) == NULL)
777 bool Translator::GetLanguageByName(const char *name, unsigned int *index)
780 size_t len = strlen(name);
781 if (len > 255) len = 255;
782 for (size_t i = 0; i < len; i++)
784 if (name[i] >= 'A' && name[i] <= 'Z')
785 lower[i] = tolower(name[i]);
791 unsigned int *pIndex;
793 if ((pIndex = m_LAliases.retrieve(lower)) == NULL)
802 unsigned int Translator::GetLanguageCount()
804 return (unsigned int)m_Languages.size();
807 BaseStringTable *Translator::GetStringTable()
812 unsigned int Translator::FindOrAddPhraseFile(const char *phrase_file)
814 for (size_t i=0; i<m_Files.size(); i++)
816 if (strcmp(m_Files[i]->GetFilename(), phrase_file) == 0)
818 return (unsigned int)i;
822 CPhraseFile *pFile = new CPhraseFile(this, phrase_file);
823 unsigned int idx = (unsigned int)m_Files.size();
825 m_Files.push_back(pFile);
827 pFile->ReparseFile();
832 void Translator::RebuildLanguageDatabase(const char *lang_header_file)
834 /* Erase everything we have */
835 m_LCodeLookup.clear();
837 m_pStringTab->Reset();
839 for (size_t i=0; i<m_Languages.size(); i++)
841 delete m_Languages[i];
848 if ((err=textparsers->ParseFile_SMC(lang_header_file, this, &states)) != SMCError_Okay)
850 const char *str_err = textparsers->GetSMCErrorString(err);
853 str_err = m_CustomError.c_str();
856 smcore.LogError("[SM] Failed to parse language header file: \"%s\"", lang_header_file);
857 smcore.LogError("[SM] Parse error (line %d, column %d): %s", states.line, states.col, str_err);
860 unsigned int *pServerLang;
862 if ((pServerLang = m_LCodeLookup.retrieve(m_InitialLang)) == NULL)
864 smcore.LogError("Server language was set to bad language \"%s\" -- reverting to English", m_InitialLang);
866 smcore.strncopy(m_InitialLang, "en", sizeof(m_InitialLang));
867 m_ServerLang = SOURCEMOD_LANGUAGE_ENGLISH;
871 m_ServerLang = *pServerLang;
874 if (!m_Languages.size())
876 smcore.LogError("[SM] Fatal error, no languages found! Translation will not work.");
879 for (size_t i=0; i<m_Files.size(); i++)
881 m_Files[i]->ReparseFile();
885 void Translator::ReadSMC_ParseStart()
887 m_InLanguageSection = false;
888 m_CustomError.clear();
891 SMCResult Translator::ReadSMC_NewSection(const SMCStates *states, const char *name)
893 if (!m_InLanguageSection)
895 if (strcmp(name, "Languages") == 0)
897 m_InLanguageSection = true;
901 if (!m_InLanguageSection)
903 smcore.LogError("[SM] Warning: Unrecognized section \"%s\" in languages.cfg", name);
906 return SMCResult_Continue;
909 SMCResult Translator::ReadSMC_LeavingSection(const SMCStates *states)
911 m_InLanguageSection = false;
913 return SMCResult_Continue;
916 SMCResult Translator::ReadSMC_KeyValue(const SMCStates *states, const char *key, const char *value)
918 size_t len = strlen(key);
920 if (len < 2 || len > 3)
922 smcore.LogError("[SM] Warning encountered parsing languages.cfg file.");
923 smcore.LogError("[SM] Invalid language code \"%s\" is being ignored.", key);
926 AddLanguage(key, value);
928 return SMCResult_Continue;
931 bool Translator::AddLanguage(const char *langcode, const char *description)
934 size_t len = strlen(description);
935 if (len > 255) len = 255;
936 for (size_t i = 0; i < len; i++)
938 if (description[i] >= 'A' && description[i] <= 'Z')
939 lower[i] = tolower(description[i]);
941 lower[i] = description[i];
945 if (m_LAliases.retrieve(lower))
949 if (unsigned int *pIdx = m_LCodeLookup.retrieve(langcode))
955 Language *pLanguage = new Language;
956 idx = m_Languages.size();
958 smcore.Format(pLanguage->m_code2, sizeof(pLanguage->m_code2), "%s", langcode);
959 pLanguage->m_CanonicalName = m_pStringTab->AddString(lower);
961 m_LCodeLookup.insert(langcode, idx);
963 m_Languages.push_back(pLanguage);
966 m_LAliases.insert(lower, idx);
971 CPhraseFile *Translator::GetFileByIndex(unsigned int index)
973 if (index >= m_Files.size())
978 return m_Files[index];
981 unsigned int Translator::GetServerLanguage()
986 unsigned int Translator::GetClientLanguage(int client)
988 IGamePlayer *pPlayer = playerhelpers->GetGamePlayer(client);
989 return pPlayer->GetLanguageId();
992 bool Translator::GetLanguageInfo(unsigned int number, const char **code, const char **name)
994 if (number >= GetLanguageCount())
999 Language *l = m_Languages[number];
1006 *name = m_pStringTab->GetString(l->m_CanonicalName);
1012 const char *Translator::GetInterfaceName()
1014 return SMINTERFACE_TRANSLATOR_NAME;
1017 unsigned int Translator::GetInterfaceVersion()
1019 return SMINTERFACE_TRANSLATOR_VERSION;
1022 IPhraseCollection *Translator::CreatePhraseCollection()
1024 return new CPhraseCollection();
1027 int Translator::SetGlobalTarget(int index)
1029 return g_pSM->SetGlobalTarget(index);
1032 int Translator::GetGlobalTarget() const
1034 return g_pSM->GetGlobalTarget();
1037 bool CoreTranslate(char *buffer, size_t maxlength, const char *format, unsigned int numparams,
1038 size_t *pOutLength, ...)
1042 const char *fail_phrase;
1043 void *params[MAX_TRANSLATE_PARAMS];
1045 if (numparams > MAX_TRANSLATE_PARAMS)
1051 va_start(ap, pOutLength);
1052 for (i = 0; i < numparams; i++)
1054 params[i] = va_arg(ap, void *);
1058 if (!g_pCorePhrases->FormatString(buffer,
1066 if (fail_phrase != NULL)
1068 smcore.LogError("[SM] Could not find core phrase: %s", fail_phrase);
1072 smcore.LogError("[SM] Unknown fatal error while translating a core phrase.");
1081 bool Translator::FormatString(char *buffer,
1084 IPhraseCollection *pPhrases,
1086 unsigned int numparams,
1088 const char **pFailPhrase)
1093 if (!smcore.gnprintf(buffer, maxlength, format, pPhrases, params, numparams, arg, pOutLength, pFailPhrase))
1098 if (arg != numparams)
1100 if (pFailPhrase != NULL)
1102 *pFailPhrase = NULL;