/** * vim: set ts=4 : * ============================================================================= * SourceMod * Copyright (C) 2004-2008 AlliedModders LLC. All rights reserved. * ============================================================================= * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License, version 3.0, as published by the * Free Software Foundation. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License along with * this program. If not, see . * * As a special exception, AlliedModders LLC gives you permission to link the * code of this program (as well as its derivative works) to "Half-Life 2," the * "Source Engine," the "SourcePawn JIT," and any Game MODs that run on software * by the Valve Corporation. You must obey the GNU General Public License in * all respects for all other code used. Additionally, AlliedModders LLC grants * this exception to all derivative works. AlliedModders LLC defines further * exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007), * or . * * Version: $Id$ */ #include #include #include #include "Translator.h" #include "Logger.h" #include "LibrarySys.h" #include "sm_stringutil.h" #include "sourcemod.h" #include "PlayerManager.h" #include "PhraseCollection.h" #include "ShareSys.h" Translator g_Translator; IPhraseCollection *g_pCorePhrases = NULL; unsigned int g_pCorePhraseID = 0; struct trans_t { int stridx; int fmt_order; }; struct phrase_t { int fmt_list; unsigned int fmt_count; unsigned int fmt_bytes; int trans_tbl; unsigned int translations; }; CPhraseFile::CPhraseFile(Translator *pTranslator, const char *file) { m_pStringTab = pTranslator->GetStringTable(); m_pMemory = m_pStringTab->GetMemTable(); m_LangCount = pTranslator->GetLanguageCount(); m_File.assign(file); m_pTranslator = pTranslator; m_pPhraseLookup = NULL; } CPhraseFile::~CPhraseFile() { if (m_pPhraseLookup) { sm_trie_destroy(m_pPhraseLookup); } } void CPhraseFile::ParseError(const char *message, ...) { char buffer[1024]; va_list ap; va_start(ap, message); vsnprintf(buffer, sizeof(buffer), message, ap); va_end(ap); m_ParseError.assign(buffer); } void CPhraseFile::ParseWarning(const char *message, ...) { char buffer[1024]; va_list ap; va_start(ap, message); vsnprintf(buffer, sizeof(buffer), message, ap); va_end(ap); if (!m_FileLogged) { g_Logger.LogError("[SM] Warning(s) encountered in translation file \"%s\"", m_File.c_str()); m_FileLogged = true; } g_Logger.LogError("[SM] %s", message); } void CPhraseFile::ReparseFile() { if (m_pPhraseLookup) { sm_trie_destroy(m_pPhraseLookup); } m_pPhraseLookup = sm_trie_create(); m_LangCount = m_pTranslator->GetLanguageCount(); if (!m_LangCount) { return; } SMCError err; char path[PLATFORM_MAX_PATH]; g_SourceMod.BuildPath(Path_SM, path, PLATFORM_MAX_PATH, "translations/%s", m_File.c_str()); //backwards compatibility shim /* :HACKHACK: Change .cfg/.txt and vice versa for compatibility */ if (!g_LibSys.PathExists(path)) { if (m_File.compare("common.cfg") == 0) { UTIL_ReplaceAll(path, sizeof(path), "common.cfg", "common.phrases.txt"); } else if (strstr(path, ".cfg")) { UTIL_ReplaceAll(path, sizeof(path), ".cfg", ".txt"); } else if (strstr(path, ".txt")) { UTIL_ReplaceAll(path, sizeof(path), ".txt", ".cfg"); } } SMCStates states; if ((err=textparsers->ParseFile_SMC(path, this, &states)) != SMCError_Okay) { const char *msg = textparsers->GetSMCErrorString(err); if (!msg) { msg = m_ParseError.c_str(); } g_Logger.LogError("[SM] Fatal error encountered parsing translation file \"%s\"", m_File.c_str()); g_Logger.LogError("[SM] Error (line %d, column %d): %s", states.line, states.col, msg); } } void CPhraseFile::ReadSMC_ParseStart() { m_CurPhrase = -1; m_ParseState = PPS_None; m_FileLogged = false; m_LastPhraseString.clear(); } SMCResult CPhraseFile::ReadSMC_NewSection(const SMCStates *states, const char *name) { bool recognized = false; if (m_ParseState == PPS_None) { if (strcmp(name, "Phrases") == 0) { m_ParseState = PPS_Phrases; recognized = true; } } else if (m_ParseState == PPS_Phrases) { m_ParseState = PPS_InPhrase; recognized = true; phrase_t *pPhrase; m_CurPhrase = m_pMemory->CreateMem(sizeof(phrase_t), (void **)&pPhrase); /* Create the reverse lookup */ if (!sm_trie_insert(m_pPhraseLookup, name, reinterpret_cast(m_CurPhrase))) { ParseWarning("Skipping duplicate phrase \"%s\" on line %d.", name, states->line); /* :IDEA: prevent memory waste by seeking backwards in memtable? */ m_CurPhrase = -1; } else { /* Initialize new phrase */ trans_t *pTrans; pPhrase->fmt_count = 0; pPhrase->fmt_list = -1; int trans_tbl = m_pMemory->CreateMem(sizeof(trans_t) * m_LangCount, (void **)&pTrans); pPhrase = (phrase_t *)m_pMemory->GetAddress(m_CurPhrase); pPhrase->trans_tbl = trans_tbl; pPhrase->translations = 0; pPhrase->fmt_bytes = 0; for (unsigned int i=0; iline); } return SMCResult_Continue; } SMCResult CPhraseFile::ReadSMC_KeyValue(const SMCStates *states, const char *key, const char *value) { /* See if we are ignoring a phrase */ if (m_CurPhrase == -1) { return SMCResult_Continue; } phrase_t *pPhrase = (phrase_t *)m_pMemory->GetAddress(m_CurPhrase); if (key[0] == '#' && strcmp(key, "#format") == 0) { if (pPhrase->fmt_list != -1) { ParseWarning("Ignoring duplicated #format property on line %d", states->line); return SMCResult_Continue; } if (pPhrase->translations > 0) { ParseWarning("#format property should come before translations on line %d, ignoring", states->line); return SMCResult_Continue; } /* Do an initial browsing for error checking and what not */ enum ParseStates { Parse_None, Parse_Index, Parse_Format, }; ParseStates state = Parse_None; const char *old_value = value; const char *last_value = value; while (*value != '\0') { if (state == Parse_None) { if (*value == '{') { pPhrase->fmt_count++; state = Parse_Index; } else if (*value == ',') { /* Do nothing */ } else { unsigned int bytes = textparsers->GetUTF8CharBytes(value); if (bytes != 1 || !isalpha(*value)) { ParseWarning("Invalid token '%c' in #format property on line %d.", *value, states->line); } } } else if (state == Parse_Index) { if (*value == ':') { state = Parse_Format; if (value - last_value >= 15) { ParseWarning("Too many digits in format index on line %d, phrase will be ignored.", states->line); m_CurPhrase = -1; return SMCResult_Continue; } } else { unsigned int bytes = textparsers->GetUTF8CharBytes(value); if (bytes != 1 || !isdigit(*value)) { ParseWarning("Token '%c' in #format property on line %d is not a digit, phrase will be ignored.", *value, states->line); m_CurPhrase = -1; return SMCResult_Continue; } } } else if (state == Parse_Format) { if (*value == '}') { state = Parse_None; last_value = value + 1; } } value++; } if (state != Parse_None) { /* Moose clam cow. */ ParseWarning("Unterminated format string on line %d, phrase will be ignored.", states->line); m_CurPhrase = -1; return SMCResult_Continue; } value = old_value; /* Allocate the format table */ char fmt_buf[16]; int *fmt_list; int tmp = m_pMemory->CreateMem(sizeof(int) * pPhrase->fmt_count, (void **)&fmt_list); /* Update the phrase pointer in case it changed */ pPhrase = (phrase_t *)m_pMemory->GetAddress(m_CurPhrase); pPhrase->fmt_list = tmp; /* Initialize */ for (size_t i=0; ifmt_count; i++) { fmt_list[i] = -1; } /* Now we need to read again... this time into the format buffer */ const char *in_ptr = value; const char *idx_ptr = NULL; char *out_ptr = NULL; unsigned int cur_idx = 0; state = Parse_None; while (*in_ptr != '\0') { if (state == Parse_None) { if (*in_ptr == '{') { state = Parse_Index; idx_ptr = NULL; } } else if (state == Parse_Index) { if (*in_ptr == ':') { /* Check the number! */ if (!idx_ptr) { ParseWarning("Format property contains unindexed format string on line %d, phrase will be ignored.", states->line); m_CurPhrase = -1; return SMCResult_Continue; } long idx = strtol(idx_ptr, NULL, 10); if (idx < 1 || idx > (long)pPhrase->fmt_count) { ParseWarning("Format property contains invalid index '%d' on line %d, phrase will be ignored.", idx, states->line); m_CurPhrase = -1; return SMCResult_Continue; } else if (fmt_list[idx - 1] != -1) { ParseWarning("Format property contains duplicated index '%d' on line %d, phrase will be ignored.", idx, states->line); m_CurPhrase = -1; return SMCResult_Continue; } cur_idx = (unsigned int)idx; state = Parse_Format; out_ptr = NULL; } else if (!idx_ptr) { idx_ptr = in_ptr; } } else if (state == Parse_Format) { if (*in_ptr == '}') { if (!out_ptr) { ParseWarning("Format property contains empty format string on line %d, phrase will be ignored.", states->line); m_CurPhrase = -1; return SMCResult_Continue; } *out_ptr = '\0'; state = Parse_None; /* Now, add this to our table */ int tmp_idx = m_pStringTab->AddString(fmt_buf); /* Update pointers and update necessary variables */ pPhrase = (phrase_t *)m_pMemory->GetAddress(m_CurPhrase); pPhrase->fmt_bytes += strlen(fmt_buf); fmt_list = (int *)m_pMemory->GetAddress(pPhrase->fmt_list); fmt_list[cur_idx - 1] = tmp_idx; } else { if (!out_ptr) { out_ptr = fmt_buf; *out_ptr++ = '%'; } /* Check length ... */ if ((unsigned)(out_ptr - fmt_buf) >= sizeof(fmt_buf) - 1) { ParseWarning("Format property contains format string that exceeds maximum length on line %d, phrase will be ignored.", states->line); m_CurPhrase = -1; return SMCResult_Continue; } *out_ptr++ = *in_ptr; } } in_ptr++; } /* If we've gotten here, we only accepted unique indexes in a range. * Therefore, the list has to be completely filled. Double check anyway. */ for (size_t i=0; ifmt_count; i++) { if (fmt_list[i] == -1) { ParseWarning("Format property contains no string for index %d on line %d, phrase will be ignored.", i + 1, states->line); m_CurPhrase = -1; return SMCResult_Continue; } } } else { size_t len = strlen(key); if (len != 2) { ParseWarning("Ignoring translation to invalid language \"%s\" on line %d.", key, states->line); return SMCResult_Continue; } unsigned int lang; if (!m_pTranslator->GetLanguageByCode(key, &lang)) { /* Ignore if we don't have a language. * :IDEA: issue a one-time alert? */ return SMCResult_Continue; } /* See how many bytes we need for this string, then allocate. * NOTE: THIS SHOULD GUARANTEE THAT WE DO NOT NEED TO NEED TO SIZE CHECK */ len = strlen(value) + pPhrase->fmt_bytes + 1; char *out_buf; int out_idx; out_idx = m_pMemory->CreateMem(len, (void **)&out_buf); /* Update pointer */ pPhrase = (phrase_t *)m_pMemory->GetAddress(m_CurPhrase); int *fmt_order; int *fmt_list = (int *)m_pMemory->GetAddress(pPhrase->fmt_list); trans_t *pTrans = (trans_t *)m_pMemory->GetAddress(pPhrase->trans_tbl); pTrans = &pTrans[lang]; pTrans->stridx = out_idx; bool params[MAX_TRANSLATE_PARAMS]; /* Build the format order list, if necessary */ if (fmt_list) { int tmp = m_pMemory->CreateMem(pPhrase->fmt_count * sizeof(int), (void **)&fmt_order); /* Update pointers */ pPhrase = (phrase_t *)m_pMemory->GetAddress(m_CurPhrase); pTrans = (trans_t *)m_pMemory->GetAddress(pPhrase->trans_tbl); fmt_list = (int *)m_pMemory->GetAddress(pPhrase->fmt_list); out_buf = (char *)m_pMemory->GetAddress(out_idx); pTrans = &pTrans[lang]; /* Now it's safe to save the index */ pTrans->fmt_order = tmp; for (unsigned int i=0; ifmt_count; i++) { fmt_order[i] = -1; } memset(¶ms[0], 0, sizeof(params)); } const char *in_ptr = value; char *out_ptr = out_buf; unsigned int order_idx = 0; while (*in_ptr != '\0') { if (*in_ptr == '\\') { switch (*(in_ptr + 1)) { case '\n': *out_ptr++ = '\n'; break; case '\t': *out_ptr++ = '\t'; break; case '\r': *out_ptr++ = '\r'; break; case '{': *out_ptr++ = '{'; break; default: /* Copy both bytes since we don't know what's going on */ *out_ptr++ = *in_ptr++; *out_ptr++ = *in_ptr; break; } /* Skip past the last byte read */ in_ptr++; } else if (*in_ptr == '{' && fmt_list != NULL) { /* Search for parameters if this is a formatted string */ const char *scrap_in_point = in_ptr; const char *digit_start = ++in_ptr; unsigned int bytes; while (*in_ptr != '\0') { bytes = textparsers->GetUTF8CharBytes(in_ptr); if (bytes != 1) { goto scrap_point; } if (*in_ptr == '}') { /* Did we get an index? */ if (in_ptr == digit_start) { goto scrap_point; } /* Is it valid? */ long idx = strtol(digit_start, NULL, 10); if (idx < 1 || idx > (int)pPhrase->fmt_count) { goto scrap_point; } if (params[idx-1]) { goto scrap_point; } /* We're safe to insert the string. First mark order. */ fmt_order[order_idx++] = (int)idx - 1; /* Now concatenate */ out_ptr += sprintf(out_ptr, "%s", m_pStringTab->GetString(fmt_list[idx-1])); /* Mark as used */ params[idx-1] = true; goto cont_loop; } in_ptr++; } scrap_point: /* Pretend none of this ever happened. Move along now! */ in_ptr = scrap_in_point; *out_ptr++ = *in_ptr; } else { *out_ptr++ = *in_ptr; } cont_loop: in_ptr++; } *out_ptr = '\0'; pPhrase->translations++; } return SMCResult_Continue; } SMCResult CPhraseFile::ReadSMC_LeavingSection(const SMCStates *states) { if (m_ParseState == PPS_InPhrase) { if (m_CurPhrase == -1 && m_LastPhraseString.size()) { sm_trie_delete(m_pPhraseLookup, m_LastPhraseString.c_str()); } m_CurPhrase = -1; m_ParseState = PPS_Phrases; m_LastPhraseString.assign(""); } else if (m_ParseState == PPS_Phrases) { m_ParseState = PPS_None; } return SMCResult_Continue; } void CPhraseFile::ReadSMC_ParseEnd(bool halted, bool failed) { /* Check to see if we have any dangling phrases that weren't completed, and scrap them */ if ((halted || failed) && m_LastPhraseString.size()) { sm_trie_delete(m_pPhraseLookup, m_LastPhraseString.c_str()); } } TransError CPhraseFile::GetTranslation(const char *szPhrase, unsigned int lang_id, Translation *pTrans) { if (lang_id >= m_LangCount) { return Trans_BadLanguage; } void *object; if (!sm_trie_retrieve(m_pPhraseLookup, szPhrase, &object)) { return Trans_BadPhrase; } phrase_t *pPhrase = (phrase_t *)m_pMemory->GetAddress(reinterpret_cast(object)); trans_t *trans = (trans_t *)m_pMemory->GetAddress(pPhrase->trans_tbl); trans = &trans[lang_id]; if (trans->stridx == -1) { return Trans_BadPhraseLanguage; } pTrans->fmt_count = pPhrase->fmt_count; pTrans->fmt_order = pTrans->fmt_count > 0 ? (int *)m_pMemory->GetAddress(trans->fmt_order) : NULL; pTrans->szPhrase = m_pStringTab->GetString(trans->stridx); return Trans_Okay; } const char *CPhraseFile::GetFilename() { return m_File.c_str(); } /************************** ** MAIN TRANSLATOR CODE ** **************************/ Translator::Translator() : m_ServerLang(SOURCEMOD_LANGUAGE_ENGLISH) { m_pStringTab = new BaseStringTable(2048); m_pLCodeLookup = sm_trie_create(); strncopy(m_InitialLang, "en", sizeof(m_InitialLang)); } Translator::~Translator() { for (size_t i=0; iAddPhraseFile("core.phrases"); g_ShareSys.AddInterface(NULL, this); } void Translator::OnSourceModShutdown() { g_pCorePhrases->Destroy(); } bool Translator::GetLanguageByCode(const char *code, unsigned int *index) { void *_index; if (!sm_trie_retrieve(m_pLCodeLookup, code, &_index)) { return false; } if (index) { *index = reinterpret_cast(_index); } return true; } bool Translator::GetLanguageByName(const char *name, unsigned int *index) { CVector::iterator iter; unsigned int id = 0; for (iter=m_Languages.begin(); iter!=m_Languages.end(); iter++, id++) { if (strcasecmp(m_pStringTab->GetString((*iter)->m_FullName), name) == 0) { break; } } if (iter == m_Languages.end()) { return false; } if (index) { *index = id; } return true; } unsigned int Translator::GetLanguageCount() { return (unsigned int)m_Languages.size(); } BaseStringTable *Translator::GetStringTable() { return m_pStringTab; } unsigned int Translator::FindOrAddPhraseFile(const char *phrase_file) { for (size_t i=0; iGetFilename(), phrase_file) == 0) { return (unsigned int)i; } } CPhraseFile *pFile = new CPhraseFile(this, phrase_file); unsigned int idx = (unsigned int)m_Files.size(); m_Files.push_back(pFile); pFile->ReparseFile(); return idx; } void Translator::RebuildLanguageDatabase(const char *lang_header_file) { /* Erase everything we have */ sm_trie_destroy(m_pLCodeLookup); m_pLCodeLookup = sm_trie_create(); m_pStringTab->Reset(); for (size_t i=0; iParseFile_SMC(lang_header_file, this, &states)) != SMCError_Okay) { const char *str_err = textparsers->GetSMCErrorString(err); if (!str_err) { str_err = m_CustomError.c_str(); } g_Logger.LogError("[SM] Failed to parse language header file: \"%s\"", lang_header_file); g_Logger.LogError("[SM] Parse error (line %d, column %d): %s", states.line, states.col, str_err); } void *serverLang; if (!sm_trie_retrieve(m_pLCodeLookup, m_InitialLang, &serverLang)) { g_Logger.LogError("Server language was set to bad language \"%s\" -- reverting to English", m_InitialLang); strncopy(m_InitialLang, "en", sizeof(m_InitialLang)); m_ServerLang = SOURCEMOD_LANGUAGE_ENGLISH; } m_ServerLang = reinterpret_cast(serverLang); if (!m_Languages.size()) { g_Logger.LogError("[SM] Fatal error, no languages found! Translation will not work."); } for (size_t i=0; iReparseFile(); } } void Translator::ReadSMC_ParseStart() { m_InLanguageSection = false; m_CustomError.clear(); } SMCResult Translator::ReadSMC_NewSection(const SMCStates *states, const char *name) { if (!m_InLanguageSection) { if (strcmp(name, "Languages") == 0) { m_InLanguageSection = true; } } if (!m_InLanguageSection) { g_Logger.LogError("[SM] Warning: Unrecognized section \"%s\" in languages.cfg", name); } return SMCResult_Continue; } SMCResult Translator::ReadSMC_LeavingSection(const SMCStates *states) { m_InLanguageSection = false; return SMCResult_Continue; } SMCResult Translator::ReadSMC_KeyValue(const SMCStates *states, const char *key, const char *value) { size_t len = strlen(key); if (len != 2) { g_Logger.LogError("[SM] Warning encountered parsing languages.cfg file."); g_Logger.LogError("[SM] Invalid language code \"%s\" is being ignored.", key); } AddLanguage(key, value); return SMCResult_Continue; } bool Translator::AddLanguage(const char *langcode, const char *description) { if (sm_trie_retrieve(m_pLCodeLookup, langcode, NULL)) { return false; } Language *pLanguage = new Language; unsigned int idx = m_Languages.size(); pLanguage->m_code2[0] = langcode[0]; pLanguage->m_code2[1] = langcode[1]; pLanguage->m_code2[2] = langcode[2]; pLanguage->m_FullName = m_pStringTab->AddString(description); sm_trie_insert(m_pLCodeLookup, langcode, reinterpret_cast(idx)); m_Languages.push_back(pLanguage); return true; } CPhraseFile *Translator::GetFileByIndex(unsigned int index) { if (index >= m_Files.size()) { return NULL; } return m_Files[index]; } unsigned int Translator::GetServerLanguage() { return m_ServerLang; } unsigned int Translator::GetClientLanguage(int client) { CPlayer *pPlayer = g_Players.GetPlayerByIndex(client); return pPlayer->GetLanguageId(); } bool Translator::GetLanguageInfo(unsigned int number, const char **code, const char **name) { if (number >= GetLanguageCount()) { return false; } Language *l = m_Languages[number]; if (code) { *code = l->m_code2; } if (name) { *name = m_pStringTab->GetString(l->m_FullName); } return true; } const char *Translator::GetInterfaceName() { return SMINTERFACE_TRANSLATOR_NAME; } unsigned int Translator::GetInterfaceVersion() { return SMINTERFACE_TRANSLATOR_VERSION; } IPhraseCollection *Translator::CreatePhraseCollection() { return new CPhraseCollection(); } int Translator::SetGlobalTarget(int index) { return g_SourceMod.SetGlobalTarget(index); } int Translator::GetGlobalTarget() const { return g_SourceMod.GetGlobalTarget(); } bool CoreTranslate(char *buffer, size_t maxlength, const char *format, unsigned int numparams, size_t *pOutLength, ...) { va_list ap; unsigned int i; const char *fail_phrase; void *params[MAX_TRANSLATE_PARAMS]; if (numparams > MAX_TRANSLATE_PARAMS) { assert(false); return false; } va_start(ap, pOutLength); for (i = 0; i < numparams; i++) { params[i] = va_arg(ap, void *); } va_end(ap); if (!g_pCorePhrases->FormatString(buffer, maxlength, format, params, numparams, pOutLength, &fail_phrase)) { if (fail_phrase != NULL) { g_Logger.LogError("[SM] Could not find core phrase: %s", fail_phrase); } else { g_Logger.LogError("[SM] Unknown fatal error while translating a core phrase."); } return false; } return true; } bool Translator::FormatString(char *buffer, size_t maxlength, const char *format, IPhraseCollection *pPhrases, void **params, unsigned int numparams, size_t *pOutLength, const char **pFailPhrase) { unsigned int arg; arg = 0; if (!gnprintf(buffer, maxlength, format, pPhrases, params, numparams, arg, pOutLength, pFailPhrase)) { return false; } if (arg != numparams) { if (pFailPhrase != NULL) { *pFailPhrase = NULL; } return false; } return true; }