From 90c8287f96649d72c22708e765028d4b8b3024d1 Mon Sep 17 00:00:00 2001 From: Mohab <133429578+MohabCodeX@users.noreply.github.com> Date: Sat, 23 Aug 2025 13:58:37 +0300 Subject: [PATCH 1/5] Implement Localization support --- .../deathmatch/logic/CDownloadableResource.h | 1 + .../mods/deathmatch/logic/CPacketHandler.cpp | 17 ++ Client/mods/deathmatch/logic/CResource.cpp | 40 ++- Client/mods/deathmatch/logic/CResource.h | 8 + .../logic/CResourceTranslationItem.cpp | 31 +++ .../logic/CResourceTranslationItem.h | 28 ++ .../mods/deathmatch/logic/lua/CLuaManager.cpp | 5 +- .../luadefs/CLuaTranslationDefsClient.cpp | 177 ++++++++++++ .../logic/luadefs/CLuaTranslationDefsClient.h | 25 ++ Client/mods/deathmatch/premake5.lua | 11 +- Server/mods/deathmatch/logic/CGame.cpp | 1 + Server/mods/deathmatch/logic/CResource.cpp | 80 +++++- Server/mods/deathmatch/logic/CResource.h | 7 + Server/mods/deathmatch/logic/CResourceFile.h | 1 + .../logic/CResourceTranslationItem.cpp | 60 ++++ .../logic/CResourceTranslationItem.h | 32 +++ .../mods/deathmatch/logic/lua/CLuaManager.cpp | 2 + .../logic/luadefs/CLuaTranslationDefs.cpp | 99 +++++++ .../logic/luadefs/CLuaTranslationDefs.h | 23 ++ .../logic/packets/CResourceStartPacket.cpp | 10 +- Server/mods/deathmatch/premake5.lua | 43 +-- .../logic/CResourceTranslationManager.cpp | 262 ++++++++++++++++++ .../logic/CResourceTranslationManager.h | 57 ++++ 23 files changed, 989 insertions(+), 31 deletions(-) create mode 100644 Client/mods/deathmatch/logic/CResourceTranslationItem.cpp create mode 100644 Client/mods/deathmatch/logic/CResourceTranslationItem.h create mode 100644 Client/mods/deathmatch/logic/luadefs/CLuaTranslationDefsClient.cpp create mode 100644 Client/mods/deathmatch/logic/luadefs/CLuaTranslationDefsClient.h create mode 100644 Server/mods/deathmatch/logic/CResourceTranslationItem.cpp create mode 100644 Server/mods/deathmatch/logic/CResourceTranslationItem.h create mode 100644 Server/mods/deathmatch/logic/luadefs/CLuaTranslationDefs.cpp create mode 100644 Server/mods/deathmatch/logic/luadefs/CLuaTranslationDefs.h create mode 100644 Shared/mods/deathmatch/logic/CResourceTranslationManager.cpp create mode 100644 Shared/mods/deathmatch/logic/CResourceTranslationManager.h diff --git a/Client/mods/deathmatch/logic/CDownloadableResource.h b/Client/mods/deathmatch/logic/CDownloadableResource.h index f4fe9ee9633..2ffc6ba9d5a 100644 --- a/Client/mods/deathmatch/logic/CDownloadableResource.h +++ b/Client/mods/deathmatch/logic/CDownloadableResource.h @@ -33,6 +33,7 @@ class CDownloadableResource RESOURCE_FILE_TYPE_CLIENT_CONFIG, RESOURCE_FILE_TYPE_HTML, RESOURCE_FILE_TYPE_CLIENT_FILE, + RESOURCE_FILE_TYPE_TRANSLATION, }; public: diff --git a/Client/mods/deathmatch/logic/CPacketHandler.cpp b/Client/mods/deathmatch/logic/CPacketHandler.cpp index 054750b525c..a209aaef496 100644 --- a/Client/mods/deathmatch/logic/CPacketHandler.cpp +++ b/Client/mods/deathmatch/logic/CPacketHandler.cpp @@ -22,6 +22,7 @@ #include #include "net/SyncStructures.h" #include "CServerInfo.h" +#include "CResourceTranslationItem.h" using std::list; @@ -5228,6 +5229,22 @@ void CPacketHandler::Packet_ResourceStart(NetBitStreamInterface& bitStream) pDownloadableResource = pResource->AddConfigFile(szParsedChunkData, uiDownloadSize, chunkChecksum); break; + case CDownloadableResource::RESOURCE_FILE_TYPE_TRANSLATION: + { + bool isPrimary = bitStream.ReadBit(); + pDownloadableResource = pResource->AddResourceFile(CDownloadableResource::RESOURCE_FILE_TYPE_TRANSLATION, + szParsedChunkData, uiDownloadSize, chunkChecksum, true); + if (pDownloadableResource && isPrimary) + { + CResourceTranslationItem* translationItem = dynamic_cast(pDownloadableResource); + if (translationItem) + { + pResource->SetTranslationPrimary(translationItem->GetLanguage()); + } + } + + break; + } default: break; diff --git a/Client/mods/deathmatch/logic/CResource.cpp b/Client/mods/deathmatch/logic/CResource.cpp index 0219e80fde0..f54e3fb2aad 100644 --- a/Client/mods/deathmatch/logic/CResource.cpp +++ b/Client/mods/deathmatch/logic/CResource.cpp @@ -13,6 +13,7 @@ #define DECLARE_PROFILER_SECTION_CResource #include "profiler/SharedUtil.Profiler.h" #include "CServerIdManager.h" +#include "CResourceTranslationItem.h" using namespace std; @@ -83,6 +84,8 @@ CResource::CResource(unsigned short usNetID, const char* szResourceName, CClient // Move this after the CreateVirtualMachine line and heads will roll m_bOOPEnabled = bEnableOOP; m_iDownloadPriorityGroup = 0; + + m_translationManager = std::make_unique(m_strResourceName.c_str()); m_pLuaVM = m_pLuaManager->CreateVirtualMachine(this, bEnableOOP); if (m_pLuaVM) @@ -183,7 +186,18 @@ CDownloadableResource* CResource::AddResourceFile(CDownloadableResource::eResour return NULL; } - CResourceFile* pResourceFile = new CResourceFile(this, resourceType, szFileName, strBuffer, uiDownloadSize, serverChecksum, bAutoDownload); + CResourceFile* pResourceFile = nullptr; + + if (resourceType == CDownloadableResource::RESOURCE_FILE_TYPE_TRANSLATION) + { + bool isPrimary = m_translationPrimaryFlags.find(szFileName) != m_translationPrimaryFlags.end(); + pResourceFile = new CResourceTranslationItem(this, szFileName, strBuffer, uiDownloadSize, serverChecksum, isPrimary); + } + else + { + pResourceFile = new CResourceFile(this, resourceType, szFileName, strBuffer, uiDownloadSize, serverChecksum, bAutoDownload); + } + if (pResourceFile) { m_ResourceFiles.push_back(pResourceFile); @@ -278,6 +292,8 @@ void CResource::Load() } } + LoadTranslations(); + for (auto& list = m_NoClientCacheScriptList; !list.empty(); list.pop_front()) { DECLARE_PROFILER_SECTION(OnPreLoadNoClientCacheScript) @@ -520,3 +536,25 @@ void CResource::HandleDownloadedFileTrouble(CResourceFile* pResourceFile, bool b g_pClientGame->TellServerSomethingImportant(bScript ? 1002 : 1013, strMessage, 4); g_pCore->GetConsole()->Printf("Download error: %s", *strMessage); } + +bool CResource::LoadTranslations() +{ + for (CResourceFile* resourceFile : m_ResourceFiles) + { + if (resourceFile->GetResourceType() == CDownloadableResource::RESOURCE_FILE_TYPE_TRANSLATION) + { + CResourceTranslationItem* translationItem = dynamic_cast(resourceFile); + if (translationItem) + { + std::string fullPath = translationItem->GetName(); + if (FileExists(fullPath.c_str())) + { + std::string language = translationItem->GetLanguage(); + bool isPrimary = m_translationPrimaryFlags.find(language) != m_translationPrimaryFlags.end(); + m_translationManager->LoadTranslation(fullPath, isPrimary); + } + } + } + } + return true; +} diff --git a/Client/mods/deathmatch/logic/CResource.h b/Client/mods/deathmatch/logic/CResource.h index 5c1ef2af273..881c266a1d9 100644 --- a/Client/mods/deathmatch/logic/CResource.h +++ b/Client/mods/deathmatch/logic/CResource.h @@ -17,6 +17,7 @@ #include "CResourceFile.h" #include "CResourceModelStreamer.h" #include "CElementGroup.h" +#include "CResourceTranslationManager.h" #include #define MAX_RESOURCE_NAME_LENGTH 255 @@ -99,6 +100,10 @@ class CResource * @return A pointer to CResourceFile on success, null otherwise */ CResourceFile* GetResourceFile(const SString& relativePath) const; + + CResourceTranslationManager* GetTranslationManager() const noexcept { return m_translationManager.get(); } + bool LoadTranslations(); + void SetTranslationPrimary(const std::string& language) { m_translationPrimaryFlags[language] = true; } void SetRemainingNoClientCacheScripts(unsigned short usRemaining) { m_usRemainingNoClientCacheScripts = usRemaining; } void LoadNoClientCacheScript(const char* chunk, unsigned int length, const SString& strFilename); @@ -150,4 +155,7 @@ class CResource std::list m_NoClientCacheScriptList; CResourceModelStreamer m_modelStreamer{}; + + std::unique_ptr m_translationManager; + std::map m_translationPrimaryFlags; }; diff --git a/Client/mods/deathmatch/logic/CResourceTranslationItem.cpp b/Client/mods/deathmatch/logic/CResourceTranslationItem.cpp new file mode 100644 index 00000000000..1c2ccf3356b --- /dev/null +++ b/Client/mods/deathmatch/logic/CResourceTranslationItem.cpp @@ -0,0 +1,31 @@ +/***************************************************************************** +* +* PROJECT: Multi Theft Auto +* LICENSE: See LICENSE in the top level directory +* FILE: Client/mods/deathmatch/logic/CResourceTranslationItem.cpp +* PURPOSE: Resource translation item class +* +* Multi Theft Auto is available from https://www.multitheftauto.com/ +* +*****************************************************************************/ + +#include "StdInc.h" +#include "CResourceTranslationItem.h" +#include "CResource.h" +#include "CResourceTranslationManager.h" +#include "CLogger.h" +#include + +CResourceTranslationItem::CResourceTranslationItem(CResource* resource, const char* name, const char* src, uint uiDownloadSize, CChecksum serverChecksum, bool isPrimary) + : CResourceFile(resource, RESOURCE_FILE_TYPE_TRANSLATION, name, src, uiDownloadSize, serverChecksum, true), m_isPrimary(isPrimary) +{ + m_language = ExtractLanguageFromName(); +} + + +std::string CResourceTranslationItem::ExtractLanguageFromName() const +{ + // Cast away const since we know GetShortName() doesn't modify the object + std::filesystem::path path(const_cast(this)->GetShortName()); + return path.stem().string(); +} diff --git a/Client/mods/deathmatch/logic/CResourceTranslationItem.h b/Client/mods/deathmatch/logic/CResourceTranslationItem.h new file mode 100644 index 00000000000..5942a5a5722 --- /dev/null +++ b/Client/mods/deathmatch/logic/CResourceTranslationItem.h @@ -0,0 +1,28 @@ +/***************************************************************************** +* +* PROJECT: Multi Theft Auto +* LICENSE: See LICENSE in the top level directory +* FILE: Client/mods/deathmatch/logic/CResourceTranslationItem.h +* PURPOSE: Resource translation item class +* +* Multi Theft Auto is available from https://www.multitheftauto.com/ +* +*****************************************************************************/ + +#pragma once + +#include "CResourceFile.h" + +class CResourceTranslationItem : public CResourceFile +{ +public: + CResourceTranslationItem(CResource* resource, const char* name, const char* src, uint uiDownloadSize, CChecksum serverChecksum, bool isPrimary = false); + + const std::string& GetLanguage() const { return m_language; } + bool IsPrimary() const noexcept { return m_isPrimary; } + +private: + std::string ExtractLanguageFromName() const; + std::string m_language; + bool m_isPrimary; +}; diff --git a/Client/mods/deathmatch/logic/lua/CLuaManager.cpp b/Client/mods/deathmatch/logic/lua/CLuaManager.cpp index 89dadab3325..c34d33ca8a9 100644 --- a/Client/mods/deathmatch/logic/lua/CLuaManager.cpp +++ b/Client/mods/deathmatch/logic/lua/CLuaManager.cpp @@ -11,8 +11,10 @@ #include "StdInc.h" #include "../luadefs/CLuaFireDefs.h" -#include "../luadefs/CLuaClientDefs.h" +#include "luadefs/CLuaBuildingDefs.h" +#include "../luadefs/CLuaTranslationDefsClient.h" #include "../luadefs/CLuaVectorGraphicDefs.h" +#include "../luadefs/CLuaClientDefs.h" using std::list; @@ -283,4 +285,5 @@ void CLuaManager::LoadCFunctions() CLuaClientDefs::LoadFunctions(); CLuaDiscordDefs::LoadFunctions(); CLuaBuildingDefs::LoadFunctions(); + CLuaTranslationDefsClient::LoadFunctions(); } diff --git a/Client/mods/deathmatch/logic/luadefs/CLuaTranslationDefsClient.cpp b/Client/mods/deathmatch/logic/luadefs/CLuaTranslationDefsClient.cpp new file mode 100644 index 00000000000..1bd595458cc --- /dev/null +++ b/Client/mods/deathmatch/logic/luadefs/CLuaTranslationDefsClient.cpp @@ -0,0 +1,177 @@ +/***************************************************************************** + * + * PROJECT: Multi Theft Auto + * LICENSE: See LICENSE in the top level directory + * FILE: Client/mods/deathmatch/logic/luadefs/CLuaTranslationDefsClient.cpp + * PURPOSE: Client-only Lua translation definitions class + * + * Multi Theft Auto is available from https://www.multitheftauto.com/ + * + *****************************************************************************/ + +#include "StdInc.h" +#include "CLuaTranslationDefsClient.h" +#include "CScriptArgReader.h" +#include "CResourceTranslationManager.h" +#include "net/Packets.h" + +void CLuaTranslationDefsClient::LoadFunctions() +{ + constexpr static const std::pair functions[]{ + {"setCurrentTranslationLanguage", SetCurrentTranslationLanguage}, + {"getCurrentTranslationLanguage", GetCurrentTranslationLanguage}, + {"getTranslation", GetTranslation}, + {"getAvailableTranslations", GetAvailableTranslations}, + }; + + for (const auto& [name, func] : functions) + CLuaCFunctions::AddFunction(name, func); +} + +int CLuaTranslationDefsClient::SetCurrentTranslationLanguage(lua_State* luaVM) +{ + SString language; + + CScriptArgReader argStream(luaVM); + argStream.ReadString(language); + + if (!argStream.HasErrors()) + { + CLuaMain* luaMain = m_pLuaManager->GetVirtualMachine(luaVM); + if (luaMain) + { + CResource* resource = luaMain->GetResource(); + if (resource) + { + if (resource->GetTranslationManager()) + { + std::string oldLanguage = resource->GetTranslationManager()->GetClientLanguage(); + resource->GetTranslationManager()->SetClientLanguage(language); + std::string newLanguage = resource->GetTranslationManager()->GetClientLanguage(); + + if (oldLanguage != newLanguage) + { + CLuaArguments args; + args.PushString(oldLanguage); + args.PushString(newLanguage); + + if (g_pClientGame->GetLocalPlayer()) + { + g_pClientGame->GetLocalPlayer()->CallEvent("onClientTranslationLanguageChange", args, true); + } + } + + lua_pushboolean(luaVM, true); + return 1; + } + else + { + m_pScriptDebugging->LogError(luaVM, "Translation system not initialized for resource '%s'", resource->GetName()); + } + } + } + } + else + m_pScriptDebugging->LogCustom(luaVM, argStream.GetFullErrorMessage()); + + lua_pushboolean(luaVM, false); + return 1; +} + +int CLuaTranslationDefsClient::GetCurrentTranslationLanguage(lua_State* luaVM) +{ + CLuaMain* luaMain = m_pLuaManager->GetVirtualMachine(luaVM); + if (luaMain) + { + CResource* resource = luaMain->GetResource(); + if (resource) + { + if (resource->GetTranslationManager()) + { + std::string currentLanguage = resource->GetTranslationManager()->GetClientLanguage(); + lua_pushstring(luaVM, currentLanguage.c_str()); + return 1; + } + else + { + m_pScriptDebugging->LogError(luaVM, "Translation system not initialized for resource '%s'", resource->GetName()); + } + } + } + + lua_pushstring(luaVM, ""); + return 1; +} + +int CLuaTranslationDefsClient::GetTranslation(lua_State* luaVM) +{ + SString msgid; + SString language; + + CScriptArgReader argStream(luaVM); + argStream.ReadString(msgid); + argStream.ReadString(language, ""); + + if (!argStream.HasErrors()) + { + CLuaMain* luaMain = m_pLuaManager->GetVirtualMachine(luaVM); + if (luaMain) + { + CResource* resource = luaMain->GetResource(); + if (resource) + { + if (resource->GetTranslationManager()) + { + // If no language specified, use current client language + std::string targetLanguage = language.empty() ? + resource->GetTranslationManager()->GetClientLanguage() : language; + std::string result = resource->GetTranslationManager()->GetTranslation(msgid, targetLanguage); + lua_pushstring(luaVM, result.c_str()); + return 1; + } + else + { + m_pScriptDebugging->LogError(luaVM, "Translation system not initialized for resource '%s' when requesting '%s'", resource->GetName(), msgid.c_str()); + } + } + } + } + else + m_pScriptDebugging->LogCustom(luaVM, argStream.GetFullErrorMessage()); + + lua_pushstring(luaVM, msgid.c_str()); + return 1; +} + +int CLuaTranslationDefsClient::GetAvailableTranslations(lua_State* luaVM) +{ + CLuaMain* luaMain = m_pLuaManager->GetVirtualMachine(luaVM); + if (luaMain) + { + CResource* resource = luaMain->GetResource(); + if (resource) + { + if (resource->GetTranslationManager()) + { + std::vector languages = resource->GetTranslationManager()->GetAvailableLanguages(); + + lua_newtable(luaVM); + int index = 1; + for (size_t i = 0; i < languages.size(); ++i) + { + lua_pushinteger(luaVM, index++); + lua_pushstring(luaVM, languages[i].c_str()); + lua_settable(luaVM, -3); + } + return 1; + } + else + { + m_pScriptDebugging->LogError(luaVM, "Translation system not initialized for resource '%s'", resource->GetName()); + } + } + } + + lua_newtable(luaVM); + return 1; +} diff --git a/Client/mods/deathmatch/logic/luadefs/CLuaTranslationDefsClient.h b/Client/mods/deathmatch/logic/luadefs/CLuaTranslationDefsClient.h new file mode 100644 index 00000000000..233d785f6a3 --- /dev/null +++ b/Client/mods/deathmatch/logic/luadefs/CLuaTranslationDefsClient.h @@ -0,0 +1,25 @@ +/***************************************************************************** + * + * PROJECT: Multi Theft Auto + * LICENSE: See LICENSE in the top level directory + * FILE: Client/mods/deathmatch/logic/luadefs/CLuaTranslationDefsClient.h + * PURPOSE: Client-only Lua translation definitions class + * + * Multi Theft Auto is available from https://www.multitheftauto.com/ + * + *****************************************************************************/ + +#pragma once +#include "CLuaDefs.h" + +class CLuaTranslationDefsClient : public CLuaDefs +{ +public: + static void LoadFunctions(); + +private: + LUA_DECLARE(SetCurrentTranslationLanguage); + LUA_DECLARE(GetTranslation); + LUA_DECLARE(GetCurrentTranslationLanguage); + LUA_DECLARE(GetAvailableTranslations); +}; diff --git a/Client/mods/deathmatch/premake5.lua b/Client/mods/deathmatch/premake5.lua index 07013f8d8df..616b591daf9 100644 --- a/Client/mods/deathmatch/premake5.lua +++ b/Client/mods/deathmatch/premake5.lua @@ -9,7 +9,7 @@ project "Client Deathmatch" defines { "LUNASVG_BUILD", "LUA_USE_APICHECK", "SDK_WITH_BCRYPT" } links { - "Lua_Client", "pcre", "json-c", "ws2_32", "portaudio", "zlib", "cryptopp", "libspeex", "blowfish_bcrypt", "lunasvg", + "Lua_Client", "pcre", "json-c", "ws2_32", "portaudio", "zlib", "cryptopp", "libspeex", "blowfish_bcrypt", "lunasvg", "tinygettext", "../../../vendor/bass/lib/bass", "../../../vendor/bass/lib/bass_fx", "../../../vendor/bass/lib/bassmix", @@ -37,10 +37,11 @@ project "Client Deathmatch" "../../../vendor/bass", "../../../vendor/libspeex", "../../../vendor/zlib", - "../../../vendor/pcre", - "../../../vendor/json-c", - "../../../vendor/lua/src", - "../../../Shared/mods/deathmatch/logic", + "../../../vendor/pcre", + "../../../vendor/json-c", + "../../../vendor/lua/src", + "../../../vendor/tinygettext", + "../../../Shared/mods/deathmatch/logic", "../../../Shared/animation", "../../../vendor/sparsehash/src/", "../../../vendor/lunasvg/include" diff --git a/Server/mods/deathmatch/logic/CGame.cpp b/Server/mods/deathmatch/logic/CGame.cpp index 910a1722dde..afc77a97c4e 100644 --- a/Server/mods/deathmatch/logic/CGame.cpp +++ b/Server/mods/deathmatch/logic/CGame.cpp @@ -61,6 +61,7 @@ #include "packets/CPlayerClothesPacket.h" #include "packets/CPlayerWorldSpecialPropertyPacket.h" #include "packets/CServerInfoSyncPacket.h" +#include "CResourceTranslationManager.h" #include "packets/CLuaPacket.h" #include "../utils/COpenPortsTester.h" #include "../utils/CMasterServerAnnouncer.h" diff --git a/Server/mods/deathmatch/logic/CResource.cpp b/Server/mods/deathmatch/logic/CResource.cpp index 3c2286044cd..4f04725c117 100644 --- a/Server/mods/deathmatch/logic/CResource.cpp +++ b/Server/mods/deathmatch/logic/CResource.cpp @@ -22,6 +22,8 @@ #include "CResourceClientFileItem.h" #include "CResourceScriptItem.h" #include "CResourceClientScriptItem.h" +#include "CResourceTranslationItem.h" +#include "CResourceTranslationManager.h" #include "CAccessControlListManager.h" #include "CScriptDebugging.h" #include "CMapManager.h" @@ -104,6 +106,9 @@ bool CResource::Load() m_bStartedManually = false; m_bDoneDbConnectMysqlScan = false; + // Initialize translation manager + m_translationManager = std::make_unique(m_strResourceName.c_str()); + m_uiVersionMajor = 0; m_uiVersionMinor = 0; m_uiVersionRevision = 0; @@ -271,7 +276,15 @@ bool CResource::Load() // Read everything that's included. If one of these fail, delete the XML we created and return if (!ReadIncludedResources(pRoot) || !ReadIncludedMaps(pRoot) || !ReadIncludedFiles(pRoot) || !ReadIncludedScripts(pRoot) || - !ReadIncludedHTML(pRoot) || !ReadIncludedExports(pRoot) || !ReadIncludedConfigs(pRoot)) + !ReadIncludedHTML(pRoot) || !ReadIncludedExports(pRoot) || !ReadIncludedConfigs(pRoot) || !ReadIncludedTranslations(pRoot)) + { + delete pMetaFile; + g_pGame->GetHTTPD()->UnregisterEHS(m_strResourceName.c_str()); + return false; + } + + // Load translation files into the translation manager + if (!LoadTranslations()) { delete pMetaFile; g_pGame->GetHTTPD()->UnregisterEHS(m_strResourceName.c_str()); @@ -3174,7 +3187,8 @@ HttpStatusCode CResource::HandleRequestActive(HttpRequest* ipoHttpRequest, HttpR // this filename. If none match, the file not found will be sent back. else if (pResourceFile->GetType() == CResourceFile::RESOURCE_FILE_TYPE_CLIENT_CONFIG || pResourceFile->GetType() == CResourceFile::RESOURCE_FILE_TYPE_CLIENT_SCRIPT || - pResourceFile->GetType() == CResourceFile::RESOURCE_FILE_TYPE_CLIENT_FILE) + pResourceFile->GetType() == CResourceFile::RESOURCE_FILE_TYPE_CLIENT_FILE || + pResourceFile->GetType() == CResourceFile::RESOURCE_FILE_TYPE_TRANSLATION) { return pResourceFile->Request(ipoHttpRequest, ipoHttpResponse); // sends back any file in the resource } @@ -3317,6 +3331,49 @@ void CResource::OnPlayerJoin(CPlayer& Player) SendNoClientCacheScripts(&Player); } +bool CResource::ReadIncludedTranslations(CXMLNode* pRoot) +{ + int i = 0; + + for (CXMLNode* pTranslation = pRoot->FindSubNode("translation", i); pTranslation != nullptr; pTranslation = pRoot->FindSubNode("translation", ++i)) + { + CXMLAttributes& Attributes = pTranslation->GetAttributes(); + CXMLAttribute* pSrc = Attributes.Find("src"); + + if (pSrc) + { + std::string strFilename = pSrc->GetValue(); + + if (!strFilename.empty()) + { + std::string strFullFilename; + ReplaceSlashes(strFilename); + + if (IsValidFilePath(strFilename.c_str()) && GetFilePath(strFilename.c_str(), strFullFilename)) + { + m_ResourceFiles.push_back(new CResourceTranslationItem(this, strFilename.c_str(), strFullFilename.c_str(), &Attributes)); + } + else + { + m_strFailureReason = SString("Couldn't find translation %s for resource %s\n", strFilename.c_str(), m_strResourceName.c_str()); + CLogger::ErrorPrintf(m_strFailureReason); + return false; + } + } + else + { + CLogger::LogPrintf("WARNING: Empty 'src' attribute from 'translation' node of 'meta.xml' for resource '%s', ignoring\n", m_strResourceName.c_str()); + } + } + else + { + CLogger::LogPrintf("WARNING: Missing 'src' attribute from 'translation' node of 'meta.xml' for resource '%s', ignoring\n", m_strResourceName.c_str()); + } + } + + return true; +} + void CResource::SendNoClientCacheScripts(CPlayer* pPlayer) { if (!IsClientScriptsOn()) @@ -3855,3 +3912,22 @@ CResourceFile* CResource::GetResourceFile(const SString& relativePath) const return nullptr; } + +bool CResource::LoadTranslations() +{ + for (CResourceFile* pResourceFile : m_ResourceFiles) + { + CResourceTranslationItem* pTranslationItem = dynamic_cast(pResourceFile); + if (pTranslationItem) + { + std::string strFullPath = pTranslationItem->GetFullName(); + bool isPrimary = pTranslationItem->IsPrimary(); + if (!m_translationManager->LoadTranslation(strFullPath, isPrimary)) + { + m_strFailureReason = SString("Failed to load translation file '%s' for resource '%s'", pTranslationItem->GetName(), m_strResourceName.c_str()); + return false; + } + } + } + return true; +} diff --git a/Server/mods/deathmatch/logic/CResource.h b/Server/mods/deathmatch/logic/CResource.h index 3b7abb2dcbc..8f332fe5e92 100644 --- a/Server/mods/deathmatch/logic/CResource.h +++ b/Server/mods/deathmatch/logic/CResource.h @@ -34,6 +34,7 @@ class CXMLNode; class CAccount; class CLuaMain; class CResourceManager; +class CResourceTranslationManager; class CChecksum; struct SVersion @@ -330,6 +331,8 @@ class CResource : public EHS */ CResourceFile* GetResourceFile(const SString& relativePath) const; + CResourceTranslationManager* GetTranslationManager() const noexcept { return m_translationManager.get(); } + public: static std::list m_StartedResources; @@ -355,6 +358,8 @@ class CResource : public EHS bool ReadIncludedHTML(CXMLNode* pRoot); bool ReadIncludedExports(CXMLNode* pRoot); bool ReadIncludedFiles(CXMLNode* pRoot); + bool ReadIncludedTranslations(CXMLNode* pRoot); + bool LoadTranslations(); bool CreateVM(bool bEnableOOP); bool DestroyVM(); void TidyUp(); @@ -447,4 +452,6 @@ class CResource : public EHS uint m_uiFunctionRightCacheRevision = 0; CFastHashMap m_FunctionRightCacheMap; + + std::unique_ptr m_translationManager; }; diff --git a/Server/mods/deathmatch/logic/CResourceFile.h b/Server/mods/deathmatch/logic/CResourceFile.h index 4cfc3b36d06..a7a2138d033 100644 --- a/Server/mods/deathmatch/logic/CResourceFile.h +++ b/Server/mods/deathmatch/logic/CResourceFile.h @@ -34,6 +34,7 @@ class CResourceFile RESOURCE_FILE_TYPE_CLIENT_CONFIG, RESOURCE_FILE_TYPE_HTML, RESOURCE_FILE_TYPE_CLIENT_FILE, + RESOURCE_FILE_TYPE_TRANSLATION, RESOURCE_FILE_TYPE_NONE, }; // TODO: sort all client-side enums and use >= (instead of each individual type) on comparisons that use this enum? diff --git a/Server/mods/deathmatch/logic/CResourceTranslationItem.cpp b/Server/mods/deathmatch/logic/CResourceTranslationItem.cpp new file mode 100644 index 00000000000..9c2cefd65aa --- /dev/null +++ b/Server/mods/deathmatch/logic/CResourceTranslationItem.cpp @@ -0,0 +1,60 @@ +/***************************************************************************** +* +* PROJECT: Multi Theft Auto +* LICENSE: See LICENSE in the top level directory +* +* Multi Theft Auto is available from https://www.multitheftauto.com/ +* +*****************************************************************************/ + +#include "StdInc.h" +#include "CResourceTranslationItem.h" +#include "CResource.h" +#include "CResourceTranslationManager.h" +#include "CLogger.h" +#include + +CResourceTranslationItem::CResourceTranslationItem(CResource* resource, const char* name, const char* src, CXMLAttributes* xmlAttributes) + : CResourceFile(resource, name, src, xmlAttributes), m_isPrimary(false) +{ + m_type = RESOURCE_FILE_TYPE_TRANSLATION; + m_language = ExtractLanguageFromName(); + + if (xmlAttributes) + { + CXMLAttribute* primaryAttr = xmlAttributes->Find("primary"); + if (primaryAttr) + { + std::string primaryValue = primaryAttr->GetValue(); + m_isPrimary = (primaryValue == "true"); + } + } +} + +bool CResourceTranslationItem::Start() +{ + if (!std::filesystem::exists(m_strResourceFileName)) + { + CLogger::ErrorPrintf("Translation file '%s' not found for resource '%s'\n", + m_strShortName.c_str(), m_resource->GetName().c_str()); + return false; + } + + if (m_resource->GetTranslationManager()) + { + return m_resource->GetTranslationManager()->LoadTranslation(m_strResourceFileName, m_isPrimary); + } + + return false; +} + +bool CResourceTranslationItem::Stop() +{ + return true; +} + +std::string CResourceTranslationItem::ExtractLanguageFromName() const +{ + std::filesystem::path path(m_strShortName); + return path.stem().string(); +} diff --git a/Server/mods/deathmatch/logic/CResourceTranslationItem.h b/Server/mods/deathmatch/logic/CResourceTranslationItem.h new file mode 100644 index 00000000000..11171515191 --- /dev/null +++ b/Server/mods/deathmatch/logic/CResourceTranslationItem.h @@ -0,0 +1,32 @@ +/***************************************************************************** +* +* PROJECT: Multi Theft Auto +* LICENSE: See LICENSE in the top level directory +* +* Multi Theft Auto is available from https://www.multitheftauto.com/ +* +*****************************************************************************/ + +#pragma once + +#include "CResourceFile.h" + +class CResourceTranslationItem : public CResourceFile +{ +public: + CResourceTranslationItem(CResource* resource, const char* name, const char* src, CXMLAttributes* xmlAttributes = nullptr); + ~CResourceTranslationItem() = default; + + bool Start() override; + bool Stop() override; + + std::string GetLanguage() const noexcept { return m_language; } + bool IsPrimary() const noexcept { return m_isPrimary; } + +private: + std::string ExtractLanguageFromName() const; + +private: + std::string m_language; + bool m_isPrimary; +}; diff --git a/Server/mods/deathmatch/logic/lua/CLuaManager.cpp b/Server/mods/deathmatch/logic/lua/CLuaManager.cpp index a5c1264c0ea..04dd26e083b 100644 --- a/Server/mods/deathmatch/logic/lua/CLuaManager.cpp +++ b/Server/mods/deathmatch/logic/lua/CLuaManager.cpp @@ -43,6 +43,7 @@ #include "luadefs/CLuaVoiceDefs.h" #include "luadefs/CLuaWorldDefs.h" #include "luadefs/CLuaCompatibilityDefs.h" +#include "luadefs/CLuaTranslationDefs.h" extern CGame* g_pGame; @@ -216,6 +217,7 @@ void CLuaManager::LoadCFunctions() CLuaWorldDefs::LoadFunctions(); CLuaXMLDefs::LoadFunctions(); CLuaGenericDefs::LoadFunctions(); + CLuaTranslationDefs::LoadFunctions(); // Backward compatibility functions at the end, so the new function name is used in ACL CLuaCompatibilityDefs::LoadFunctions(); } diff --git a/Server/mods/deathmatch/logic/luadefs/CLuaTranslationDefs.cpp b/Server/mods/deathmatch/logic/luadefs/CLuaTranslationDefs.cpp new file mode 100644 index 00000000000..71ef02101aa --- /dev/null +++ b/Server/mods/deathmatch/logic/luadefs/CLuaTranslationDefs.cpp @@ -0,0 +1,99 @@ +/***************************************************************************** + * + * PROJECT: Multi Theft Auto + * LICENSE: See LICENSE in the top level directory + * FILE: Shared/mods/deathmatch/logic/luadefs/CLuaTranslationDefs.cpp + * PURPOSE: Shared Lua translation definitions class + * + * Multi Theft Auto is available from https://www.multitheftauto.com/ + * + *****************************************************************************/ + +#include "StdInc.h" +#include "CLuaTranslationDefs.h" +#include "CScriptArgReader.h" +#include "CResourceTranslationManager.h" +#include "CGame.h" + +void CLuaTranslationDefs::LoadFunctions() +{ + constexpr static const std::pair functions[]{ + {"getTranslation", GetTranslation}, + {"getAvailableTranslations", GetAvailableTranslations}, + }; + + for (const auto& [name, func] : functions) + CLuaCFunctions::AddFunction(name, func); +} + + + +int CLuaTranslationDefs::GetTranslation(lua_State* luaVM) +{ + SString msgid; + SString language; + + CScriptArgReader argStream(luaVM); + argStream.ReadString(msgid); + argStream.ReadString(language, ""); + + if (!argStream.HasErrors()) + { + CLuaMain* luaMain = m_pLuaManager->GetVirtualMachine(luaVM); + if (luaMain) + { + CResource* resource = luaMain->GetResource(); + if (resource) + { + if (resource->GetTranslationManager()) + { + std::string result = resource->GetTranslationManager()->GetTranslation(msgid, language); + lua_pushstring(luaVM, result.c_str()); + return 1; + } + else + { + m_pScriptDebugging->LogError(luaVM, "Translation system not initialized for resource '%s' when requesting '%s'", resource->GetName().c_str(), msgid.c_str()); + } + } + } + } + else + m_pScriptDebugging->LogCustom(luaVM, argStream.GetFullErrorMessage()); + + lua_pushstring(luaVM, msgid.c_str()); + return 1; +} + +int CLuaTranslationDefs::GetAvailableTranslations(lua_State* luaVM) +{ + CLuaMain* luaMain = m_pLuaManager->GetVirtualMachine(luaVM); + if (luaMain) + { + CResource* resource = luaMain->GetResource(); + if (resource) + { + if (resource->GetTranslationManager()) + { + std::vector languages = resource->GetTranslationManager()->GetAvailableLanguages(); + + lua_newtable(luaVM); + int index = 1; + for (size_t i = 0; i < languages.size(); ++i) + { + lua_pushinteger(luaVM, index++); + lua_pushstring(luaVM, languages[i].c_str()); + lua_settable(luaVM, -3); + } + return 1; + } + else + { + m_pScriptDebugging->LogError(luaVM, "Translation system not initialized for resource '%s'", resource->GetName().c_str()); + } + } + } + + lua_newtable(luaVM); + return 1; +} diff --git a/Server/mods/deathmatch/logic/luadefs/CLuaTranslationDefs.h b/Server/mods/deathmatch/logic/luadefs/CLuaTranslationDefs.h new file mode 100644 index 00000000000..41e432e2aa5 --- /dev/null +++ b/Server/mods/deathmatch/logic/luadefs/CLuaTranslationDefs.h @@ -0,0 +1,23 @@ +/***************************************************************************** + * + * PROJECT: Multi Theft Auto + * LICENSE: See LICENSE in the top level directory + * FILE: Shared/mods/deathmatch/logic/luadefs/CLuaTranslationDefs.h + * PURPOSE: Shared Lua translation definitions class + * + * Multi Theft Auto is available from https://www.multitheftauto.com/ + * + *****************************************************************************/ + +#pragma once +#include "CLuaDefs.h" + +class CLuaTranslationDefs : public CLuaDefs +{ +public: + static void LoadFunctions(); + +private: + LUA_DECLARE(GetTranslation); + LUA_DECLARE(GetAvailableTranslations); +}; diff --git a/Server/mods/deathmatch/logic/packets/CResourceStartPacket.cpp b/Server/mods/deathmatch/logic/packets/CResourceStartPacket.cpp index e5797393296..d2cfacaa88e 100644 --- a/Server/mods/deathmatch/logic/packets/CResourceStartPacket.cpp +++ b/Server/mods/deathmatch/logic/packets/CResourceStartPacket.cpp @@ -14,6 +14,7 @@ #include "CResourceClientScriptItem.h" #include "CResourceClientFileItem.h" #include "CResourceScriptItem.h" +#include "CResourceTranslationItem.h" #include "CChecksum.h" #include "CResource.h" #include "CDummy.h" @@ -74,7 +75,8 @@ bool CResourceStartPacket::Write(NetBitStreamInterface& BitStream) const if (((*iter)->GetType() == CResourceScriptItem::RESOURCE_FILE_TYPE_CLIENT_CONFIG && m_pResource->IsClientConfigsOn()) || ((*iter)->GetType() == CResourceScriptItem::RESOURCE_FILE_TYPE_CLIENT_SCRIPT && m_pResource->IsClientScriptsOn() && static_cast(*iter)->IsNoClientCache() == false) || - ((*iter)->GetType() == CResourceScriptItem::RESOURCE_FILE_TYPE_CLIENT_FILE && m_pResource->IsClientFilesOn())) + ((*iter)->GetType() == CResourceScriptItem::RESOURCE_FILE_TYPE_CLIENT_FILE && m_pResource->IsClientFilesOn()) || + ((*iter)->GetType() == CResourceScriptItem::RESOURCE_FILE_TYPE_TRANSLATION)) { // Write the Type of chunk to read (F - File, E - Exported Function) BitStream.Write(static_cast('F')); @@ -112,6 +114,12 @@ bool CResourceStartPacket::Write(NetBitStreamInterface& BitStream) const // write bool whether to download or not BitStream.WriteBit(pRCFItem->IsAutoDownload()); } + else if ((*iter)->GetType() == CResourceScriptItem::RESOURCE_FILE_TYPE_TRANSLATION) + { + CResourceTranslationItem* translationItem = dynamic_cast(*iter); + bool isPrimary = translationItem ? translationItem->IsPrimary() : false; + BitStream.WriteBit(isPrimary); + } } } diff --git a/Server/mods/deathmatch/premake5.lua b/Server/mods/deathmatch/premake5.lua index ddecdfb1796..d649d4c2158 100644 --- a/Server/mods/deathmatch/premake5.lua +++ b/Server/mods/deathmatch/premake5.lua @@ -11,30 +11,31 @@ project "Deathmatch" includedirs { "../../../vendor/sparsehash/src/windows" } filter {} - includedirs { - "../../../Shared/sdk", - "../../sdk", - "../../../vendor/bochs", - "../../../vendor/pme", - "../../../vendor/zip", - "../../../vendor/glob/include", - "../../../vendor/zlib", - "../../../vendor/pcre", - "../../../vendor/json-c", - "../../../vendor/lua/src", - "../../../Shared/gta", - "../../../Shared/mods/deathmatch/logic", - "../../../Shared/animation", - "../../../Shared/publicsdk/include", - "../../../vendor/sparsehash/src/", - "logic", - "utils", - "." - } + includedirs { + "../../../Shared/sdk", + "../../sdk", + "../../../vendor/bochs", + "../../../vendor/pme", + "../../../vendor/zip", + "../../../vendor/glob/include", + "../../../vendor/zlib", + "../../../vendor/pcre", + "../../../vendor/json-c", + "../../../vendor/lua/src", + "../../../vendor/tinygettext", + "../../../Shared/gta", + "../../../Shared/mods/deathmatch/logic", + "../../../Shared/animation", + "../../../Shared/publicsdk/include", + "../../../vendor/sparsehash/src/", + "logic", + "utils", + "." + } defines { "SDK_WITH_BCRYPT" } links { - "Lua_Server", "sqlite", "ehs", "cryptopp", "pme", "pcre", "json-c", "zip", "glob", "zlib", "blowfish_bcrypt", + "Lua_Server", "sqlite", "ehs", "cryptopp", "pme", "pcre", "json-c", "zip", "glob", "zlib", "blowfish_bcrypt", "tinygettext", } vpaths { diff --git a/Shared/mods/deathmatch/logic/CResourceTranslationManager.cpp b/Shared/mods/deathmatch/logic/CResourceTranslationManager.cpp new file mode 100644 index 00000000000..81242170fcd --- /dev/null +++ b/Shared/mods/deathmatch/logic/CResourceTranslationManager.cpp @@ -0,0 +1,262 @@ +/***************************************************************************** +* +* PROJECT: Multi Theft Auto +* LICENSE: See LICENSE in the top level directory +* +* Multi Theft Auto is available from https://www.multitheftauto.com/ +* +*****************************************************************************/ + +#include "StdInc.h" +#include "CResourceTranslationManager.h" +#include "dictionary_manager.hpp" +#include "language.hpp" +#include "po_parser.hpp" +#include "CLogger.h" +#include +#include +#include +#include + +CResourceTranslationManager::CResourceTranslationManager(const std::string& resourceName, const std::string& charset) + : m_resourceName(resourceName) +{ +} + +CResourceTranslationManager::~CResourceTranslationManager() +{ + Clear(); +} + +bool CResourceTranslationManager::LoadTranslation(const std::string& filePath, bool isPrimary) +{ + std::string language = ExtractLanguageFromPath(filePath); + if (language.empty()) + { + LogError("Could not determine language from file path: " + filePath); + return false; + } + + if (!std::filesystem::exists(filePath)) + { + LogError("Translation file not found: " + filePath); + return false; + } + + tinygettext::Language lang = tinygettext::Language::from_name(language); + if (!lang) + { + LogError("Invalid language code '" + language + "' extracted from: " + filePath); + return false; + } + + try { + // Create a new separate dictionary for each language instead of reusing the same one + auto dictionary = std::make_unique(); + + std::ifstream file(filePath, std::ios::binary); + if (!file.is_open()) + { + LogError("Could not open translation file: " + filePath); + return false; + } + + tinygettext::POParser::parse(filePath, file, *dictionary); + + m_translationFiles[language] = filePath; + m_dictionaries[language] = dictionary.release(); // Transfer ownership + + if (isPrimary) + { + m_primaryLanguage = language; + } + else if (m_translationFiles.size() == 1) + { + if (m_primaryLanguage.empty()) + { + m_primaryLanguage = language; + } + } + + return true; + } + catch (const std::exception& e) + { + LogError("Exception loading translation file '" + filePath + "': " + e.what()); + return false; + } +} + +std::string CResourceTranslationManager::GetTranslation(const std::string& msgid, const std::string& language) const +{ + if (msgid.empty()) + return msgid; + + std::string targetLanguage = language.empty() ? m_primaryLanguage : language; + + if (targetLanguage.empty()) + return msgid; + + if (m_translationFiles.find(targetLanguage) == m_translationFiles.end()) + { + if (!m_primaryLanguage.empty() && targetLanguage != m_primaryLanguage && + m_translationFiles.find(m_primaryLanguage) != m_translationFiles.end()) + { + targetLanguage = m_primaryLanguage; + } + else + { + return msgid; + } + } + + auto dictIt = m_dictionaries.find(targetLanguage); + if (dictIt == m_dictionaries.end() || !dictIt->second) + return msgid; + + std::string translation = dictIt->second->translate(msgid); + return translation.empty() ? msgid : translation; +} + +std::vector CResourceTranslationManager::GetAvailableLanguages() const +{ + std::vector languages; + languages.reserve(m_translationFiles.size()); + + for (const auto& pair : m_translationFiles) + { + languages.push_back(pair.first); + } + + std::sort(languages.begin(), languages.end()); + + return languages; +} + +void CResourceTranslationManager::SetPlayerLanguage(void* player, const std::string& language) +{ + if (!player || language.empty()) + return; + + if (m_translationFiles.find(language) != m_translationFiles.end()) + { + m_playerLanguages[player] = language; + } + else + { + LogWarning("Language '" + language + "' not available for player, using primary language '" + m_primaryLanguage + "'"); + if (!m_primaryLanguage.empty()) + m_playerLanguages[player] = m_primaryLanguage; + } +} + +std::string CResourceTranslationManager::GetPlayerLanguage(void* player) const +{ + if (!player) + { + if (!m_primaryLanguage.empty()) + return m_primaryLanguage; + // If no primary language, return first available language + if (!m_translationFiles.empty()) + return m_translationFiles.begin()->first; + return ""; + } + + auto it = m_playerLanguages.find(player); + if (it != m_playerLanguages.end()) + return it->second; + + // Fallback to primary language or first available language + if (!m_primaryLanguage.empty()) + return m_primaryLanguage; + if (!m_translationFiles.empty()) + return m_translationFiles.begin()->first; + return ""; +} + +void CResourceTranslationManager::RemovePlayer(void* player) +{ + if (player) + m_playerLanguages.erase(player); +} + +void CResourceTranslationManager::SetClientLanguage(const std::string& language) +{ + if (language.empty()) + { + LogWarning("SetClientLanguage called with empty language"); + return; + } + + LogWarning("SetClientLanguage called with: '" + language + "'"); + LogWarning("Available languages: " + std::to_string(m_translationFiles.size())); + for (const auto& pair : m_translationFiles) + { + LogWarning(" - " + pair.first); + } + + if (m_translationFiles.find(language) != m_translationFiles.end()) + { + m_clientLanguage = language; + LogWarning("Client language set to: '" + m_clientLanguage + "'"); + } + else + { + LogWarning("Language '" + language + "' not found, falling back to primary: '" + m_primaryLanguage + "'"); + if (!m_primaryLanguage.empty()) + { + m_clientLanguage = m_primaryLanguage; + } + else + { + LogWarning("No primary language set, keeping current: '" + m_clientLanguage + "'"); + } + } +} + +std::string CResourceTranslationManager::GetClientLanguage() const +{ + if (!m_clientLanguage.empty()) + return m_clientLanguage; + + if (!m_primaryLanguage.empty()) + return m_primaryLanguage; + + // If no primary language, return first available language + if (!m_translationFiles.empty()) + return m_translationFiles.begin()->first; + + return ""; +} + +void CResourceTranslationManager::Clear() +{ + m_translationFiles.clear(); + + // Clean up owned dictionaries + for (auto& pair : m_dictionaries) + { + delete pair.second; + } + m_dictionaries.clear(); + + m_primaryLanguage.clear(); + m_clientLanguage.clear(); + m_playerLanguages.clear(); +} + +std::string CResourceTranslationManager::ExtractLanguageFromPath(const std::string& filePath) const +{ + std::filesystem::path path(filePath); + return path.stem().string(); +} + +void CResourceTranslationManager::LogWarning(const std::string& message) const +{ + CLogger::LogPrintf("[%s] %s\n", m_resourceName.c_str(), message.c_str()); +} + +void CResourceTranslationManager::LogError(const std::string& message) const +{ + CLogger::ErrorPrintf("[%s] %s\n", m_resourceName.c_str(), message.c_str()); +} diff --git a/Shared/mods/deathmatch/logic/CResourceTranslationManager.h b/Shared/mods/deathmatch/logic/CResourceTranslationManager.h new file mode 100644 index 00000000000..94440db02ce --- /dev/null +++ b/Shared/mods/deathmatch/logic/CResourceTranslationManager.h @@ -0,0 +1,57 @@ +/***************************************************************************** +* +* PROJECT: Multi Theft Auto +* LICENSE: See LICENSE in the top level directory +* +* Multi Theft Auto is available from https://www.multitheftauto.com/ +* +*****************************************************************************/ + +#pragma once + +#include +#include +#include +#include +#include +#include "../../../vendor/tinygettext/dictionary.hpp" + +namespace tinygettext +{ + class DictionaryManager; +} + +class CResourceTranslationManager +{ +public: + explicit CResourceTranslationManager(const std::string& resourceName, const std::string& charset = "UTF-8"); + ~CResourceTranslationManager(); + + bool LoadTranslation(const std::string& filePath, bool isPrimary = false); + std::string GetTranslation(const std::string& msgid, const std::string& language = "") const; + void SetPlayerLanguage(void* player, const std::string& language); + std::string GetPlayerLanguage(void* player) const; + void RemovePlayer(void* player); + + void SetClientLanguage(const std::string& language); + std::string GetClientLanguage() const; + std::string GetPrimaryLanguage() const noexcept { return m_primaryLanguage; } + std::vector GetAvailableLanguages() const; + void Clear(); + + bool HasTranslations() const noexcept { return !m_translationFiles.empty(); } + +private: + std::string ExtractLanguageFromPath(const std::string& filePath) const; + void LogWarning(const std::string& message) const; + void LogError(const std::string& message) const; + +private: + std::string m_primaryLanguage; + std::string m_clientLanguage; + std::map m_translationFiles; + std::map m_dictionaries; + std::string m_resourceName; + + std::unordered_map m_playerLanguages; +}; From 317c63ce560dfbfd9d0e676bb5f2b8f00054ceeb Mon Sep 17 00:00:00 2001 From: Mohab <133429578+MohabCodeX@users.noreply.github.com> Date: Sun, 24 Aug 2025 00:38:53 +0300 Subject: [PATCH 2/5] Possible tinygettext fix for mac & linux --- premake5.lua | 26 ++++++++++++------------- vendor/tinygettext/dirent_win32.h | 11 +++++++++-- vendor/tinygettext/premake5.lua | 5 ++++- vendor/tinygettext/unix_file_system.cpp | 4 ++-- 4 files changed, 28 insertions(+), 18 deletions(-) diff --git a/premake5.lua b/premake5.lua index f87c8585979..986a019cd12 100644 --- a/premake5.lua +++ b/premake5.lua @@ -157,19 +157,18 @@ workspace "MTASA" include "vendor/cegui-0.4.0-custom/WidgetSets/Falagard" include "vendor/cegui-0.4.0-custom" - group "Vendor" - include "vendor/portaudio" - include "vendor/cef3" - include "vendor/discord-rpc" - include "vendor/freetype" - include "vendor/jpeg-9f" - include "vendor/ksignals" - include "vendor/libpng" - include "vendor/tinygettext" - include "vendor/pthreads" - include "vendor/libspeex" - include "vendor/detours" - include "vendor/lunasvg" + group "Vendor" + include "vendor/portaudio" + include "vendor/cef3" + include "vendor/discord-rpc" + include "vendor/freetype" + include "vendor/jpeg-9f" + include "vendor/ksignals" + include "vendor/libpng" + include "vendor/pthreads" + include "vendor/libspeex" + include "vendor/detours" + include "vendor/lunasvg" end filter {} @@ -196,6 +195,7 @@ workspace "MTASA" include "vendor/pcre" include "vendor/pme" include "vendor/sqlite" + include "vendor/tinygettext" include "vendor/tinyxml" include "vendor/unrar" include "vendor/zip" diff --git a/vendor/tinygettext/dirent_win32.h b/vendor/tinygettext/dirent_win32.h index e81a853f0c7..663d8504f4e 100644 --- a/vendor/tinygettext/dirent_win32.h +++ b/vendor/tinygettext/dirent_win32.h @@ -108,6 +108,7 @@ #include #if defined(_MSC_VER) && _MSC_VER >= 1400 #include + #include #endif /* Indicates that d_type field is available in dirent structure */ @@ -779,7 +780,10 @@ dirent_mbstowcs_s( #if defined(_MSC_VER) && _MSC_VER >= 1400 - wcscpy_s(wcstr, sizeInWords, utf8_mbstowcs(mbstr).c_str()); + std::wstring wideStr = utf8_mbstowcs(std::string(mbstr)); + wcscpy_s(wcstr, sizeInWords, wideStr.c_str()); + if (pReturnValue) + *pReturnValue = wideStr.length() + 1; error = 0; #else @@ -829,7 +833,10 @@ dirent_wcstombs_s( #if defined(_MSC_VER) && _MSC_VER >= 1400 - strcpy_s(mbstr, sizeInBytes, utf8_wcstombs(wcstr).c_str()); + std::string narrowStr = utf8_wcstombs(wcstr); + strcpy_s(mbstr, sizeInBytes, narrowStr.c_str()); + if (pReturnValue) + *pReturnValue = narrowStr.length() + 1; error = 0; #else diff --git a/vendor/tinygettext/premake5.lua b/vendor/tinygettext/premake5.lua index 852bab66ee3..58f96cd3bc3 100644 --- a/vendor/tinygettext/premake5.lua +++ b/vendor/tinygettext/premake5.lua @@ -21,4 +21,7 @@ project "tinygettext" } filter "system:windows" - disablewarnings { "4800", "4309", "4503", "4099", "4503" } + disablewarnings { "4800", "4309", "4503", "4099" } + + filter { "system:linux or macosx" } + buildoptions { "-Wno-unused-parameter", "-Wno-unused-variable" } diff --git a/vendor/tinygettext/unix_file_system.cpp b/vendor/tinygettext/unix_file_system.cpp index f98d6696a9b..6f5878266d3 100644 --- a/vendor/tinygettext/unix_file_system.cpp +++ b/vendor/tinygettext/unix_file_system.cpp @@ -21,10 +21,10 @@ #include #ifdef WIN32 #include - #include + #include "UTF8.h" #include #ifdef TGT_STANDALONE - #include + #include "UTF8.hpp" #endif #else #include From 7ae7b9cdf1bffd68ee3b8da78b4c94b13962f265 Mon Sep 17 00:00:00 2001 From: Mohab <133429578+MohabCodeX@users.noreply.github.com> Date: Mon, 25 Aug 2025 11:50:40 +0300 Subject: [PATCH 3/5] implement global translation support and fix tinygettext bug --- .../deathmatch/logic/CDownloadableResource.h | 1 + .../logic/CGlobalTranslationItem.cpp | 58 ++++ .../deathmatch/logic/CGlobalTranslationItem.h | 27 ++ .../mods/deathmatch/logic/CPacketHandler.cpp | 18 ++ Client/mods/deathmatch/logic/CResource.cpp | 37 +++ .../luadefs/CLuaTranslationDefsClient.cpp | 50 ++-- .../logic/CGlobalTranslationItem.cpp | 61 +++++ .../deathmatch/logic/CGlobalTranslationItem.h | 27 ++ Server/mods/deathmatch/logic/CResource.cpp | 62 ++++- Server/mods/deathmatch/logic/CResource.h | 1 + Server/mods/deathmatch/logic/CResourceFile.h | 1 + .../logic/luadefs/CLuaTranslationDefs.cpp | 134 +++++++-- .../logic/luadefs/CLuaTranslationDefs.h | 3 + .../logic/packets/CResourceStartPacket.cpp | 8 +- .../logic/CGlobalTranslationManager.cpp | 140 ++++++++++ .../logic/CGlobalTranslationManager.h | 44 +++ .../logic/CResourceTranslationManager.cpp | 257 ++++++++++++++++-- .../logic/CResourceTranslationManager.h | 23 ++ vendor/tinygettext/dictionary.cpp | 40 ++- vendor/tinygettext/dictionary.hpp | 11 + vendor/tinygettext/log_stream.hpp | 2 +- 21 files changed, 926 insertions(+), 79 deletions(-) create mode 100644 Client/mods/deathmatch/logic/CGlobalTranslationItem.cpp create mode 100644 Client/mods/deathmatch/logic/CGlobalTranslationItem.h create mode 100644 Server/mods/deathmatch/logic/CGlobalTranslationItem.cpp create mode 100644 Server/mods/deathmatch/logic/CGlobalTranslationItem.h create mode 100644 Shared/mods/deathmatch/logic/CGlobalTranslationManager.cpp create mode 100644 Shared/mods/deathmatch/logic/CGlobalTranslationManager.h diff --git a/Client/mods/deathmatch/logic/CDownloadableResource.h b/Client/mods/deathmatch/logic/CDownloadableResource.h index 2ffc6ba9d5a..d6e4ddfc8ca 100644 --- a/Client/mods/deathmatch/logic/CDownloadableResource.h +++ b/Client/mods/deathmatch/logic/CDownloadableResource.h @@ -34,6 +34,7 @@ class CDownloadableResource RESOURCE_FILE_TYPE_HTML, RESOURCE_FILE_TYPE_CLIENT_FILE, RESOURCE_FILE_TYPE_TRANSLATION, + RESOURCE_FILE_TYPE_GLOBAL_TRANSLATION, }; public: diff --git a/Client/mods/deathmatch/logic/CGlobalTranslationItem.cpp b/Client/mods/deathmatch/logic/CGlobalTranslationItem.cpp new file mode 100644 index 00000000000..b60032c3852 --- /dev/null +++ b/Client/mods/deathmatch/logic/CGlobalTranslationItem.cpp @@ -0,0 +1,58 @@ +/***************************************************************************** +* +* PROJECT: Multi Theft Auto +* LICENSE: See LICENSE in the top level directory +* +* Multi Theft Auto is available from https://www.multitheftauto.com/ +* +*****************************************************************************/ + +#include "StdInc.h" +#include "CGlobalTranslationItem.h" +#include "CResource.h" +#include "CResourceTranslationManager.h" +#include "CClientGame.h" + +CGlobalTranslationItem::CGlobalTranslationItem(CResource* resource, const char* src, uint uiDownloadSize, CChecksum serverChecksum) + : CResourceFile(resource, CDownloadableResource::RESOURCE_FILE_TYPE_GLOBAL_TRANSLATION, src, src, uiDownloadSize, serverChecksum, true) +{ + m_providerResourceName = src ? src : ""; +} + +bool CGlobalTranslationItem::Start() +{ + if (m_providerResourceName.empty()) + { + CScriptDebugging* scriptDebugging = g_pClientGame->GetScriptDebugging(); + SLuaDebugInfo debugInfo; + debugInfo.infoType = DEBUG_INFO_NONE; + debugInfo.strShortSrc = SString("[Resource: %s]", m_pResource->GetName()); + + scriptDebugging->LogError(debugInfo, + "Global translation: Empty provider resource name in meta.xml. " + "Check your tag."); + return false; + } + + if (m_pResource->GetTranslationManager()) + { + m_pResource->GetTranslationManager()->AddGlobalTranslationProvider(m_providerResourceName); + return true; + } + + CScriptDebugging* scriptDebugging = g_pClientGame->GetScriptDebugging(); + SLuaDebugInfo debugInfo; + debugInfo.infoType = DEBUG_INFO_NONE; + debugInfo.strShortSrc = SString("[Resource: %s]", m_pResource->GetName()); + + scriptDebugging->LogError(debugInfo, + "Global translation: Translation manager not available when adding provider '%s'. " + "This is an internal error - please report this issue.", + m_providerResourceName.c_str()); + return false; +} + +bool CGlobalTranslationItem::Stop() +{ + return true; +} \ No newline at end of file diff --git a/Client/mods/deathmatch/logic/CGlobalTranslationItem.h b/Client/mods/deathmatch/logic/CGlobalTranslationItem.h new file mode 100644 index 00000000000..fccc28ed57d --- /dev/null +++ b/Client/mods/deathmatch/logic/CGlobalTranslationItem.h @@ -0,0 +1,27 @@ +/***************************************************************************** +* +* PROJECT: Multi Theft Auto +* LICENSE: See LICENSE in the top level directory +* +* Multi Theft Auto is available from https://www.multitheftauto.com/ +* +*****************************************************************************/ + +#pragma once + +#include "CResourceFile.h" +#include + +class CGlobalTranslationItem : public CResourceFile +{ +public: + CGlobalTranslationItem(CResource* resource, const char* src, uint uiDownloadSize, CChecksum serverChecksum); + + std::string GetProviderResourceName() const noexcept { return m_providerResourceName; } + + bool Start(); + bool Stop(); + +private: + std::string m_providerResourceName; +}; \ No newline at end of file diff --git a/Client/mods/deathmatch/logic/CPacketHandler.cpp b/Client/mods/deathmatch/logic/CPacketHandler.cpp index a209aaef496..8b75e2ed597 100644 --- a/Client/mods/deathmatch/logic/CPacketHandler.cpp +++ b/Client/mods/deathmatch/logic/CPacketHandler.cpp @@ -23,6 +23,7 @@ #include "net/SyncStructures.h" #include "CServerInfo.h" #include "CResourceTranslationItem.h" +#include "CGlobalTranslationItem.h" using std::list; @@ -5245,6 +5246,13 @@ void CPacketHandler::Packet_ResourceStart(NetBitStreamInterface& bitStream) break; } + case CDownloadableResource::RESOURCE_FILE_TYPE_GLOBAL_TRANSLATION: + { + pDownloadableResource = pResource->AddResourceFile(CDownloadableResource::RESOURCE_FILE_TYPE_GLOBAL_TRANSLATION, + szParsedChunkData, uiDownloadSize, chunkChecksum, true); + + break; + } default: break; @@ -5293,6 +5301,16 @@ void CPacketHandler::Packet_ResourceStart(NetBitStreamInterface& bitStream) } } + // Read the global translation provider flag from server + bool isGlobalProvider = false; + if (bitStream.ReadBit(isGlobalProvider) && isGlobalProvider) + { + if (pResource->GetTranslationManager()) + { + pResource->GetTranslationManager()->SetAsGlobalProvider(true); + } + } + if (!bFatalError) { g_pClientGame->GetResourceFileDownloadManager()->UpdatePendingDownloads(); diff --git a/Client/mods/deathmatch/logic/CResource.cpp b/Client/mods/deathmatch/logic/CResource.cpp index f54e3fb2aad..4438eef150c 100644 --- a/Client/mods/deathmatch/logic/CResource.cpp +++ b/Client/mods/deathmatch/logic/CResource.cpp @@ -14,6 +14,8 @@ #include "profiler/SharedUtil.Profiler.h" #include "CServerIdManager.h" #include "CResourceTranslationItem.h" +#include "CGlobalTranslationItem.h" +#include "CGlobalTranslationManager.h" using namespace std; @@ -193,6 +195,10 @@ CDownloadableResource* CResource::AddResourceFile(CDownloadableResource::eResour bool isPrimary = m_translationPrimaryFlags.find(szFileName) != m_translationPrimaryFlags.end(); pResourceFile = new CResourceTranslationItem(this, szFileName, strBuffer, uiDownloadSize, serverChecksum, isPrimary); } + else if (resourceType == CDownloadableResource::RESOURCE_FILE_TYPE_GLOBAL_TRANSLATION) + { + pResourceFile = new CGlobalTranslationItem(this, szFileName, uiDownloadSize, serverChecksum); + } else { pResourceFile = new CResourceFile(this, resourceType, szFileName, strBuffer, uiDownloadSize, serverChecksum, bAutoDownload); @@ -344,6 +350,12 @@ void CResource::Load() m_bActive = true; m_bStarting = false; + // Register as global translation provider if marked as such + if (m_translationManager && m_translationManager->IsGlobalProvider()) + { + CGlobalTranslationManager::GetSingleton().RegisterProvider(m_strResourceName.c_str(), m_translationManager.get()); + } + // Did we get a resource root entity? if (m_pResourceEntity) { @@ -369,6 +381,13 @@ void CResource::Stop() { m_bStarting = false; m_bStopping = true; + + // Unregister global translation provider if this resource was one + if (m_translationManager && m_translationManager->IsGlobalProvider()) + { + CGlobalTranslationManager::GetSingleton().UnregisterProvider(m_strResourceName.c_str()); + } + CLuaArguments Arguments; Arguments.PushResource(this); m_pResourceEntity->CallEvent("onClientResourceStop", Arguments, true); @@ -555,6 +574,24 @@ bool CResource::LoadTranslations() } } } + else if (resourceFile->GetResourceType() == CDownloadableResource::RESOURCE_FILE_TYPE_GLOBAL_TRANSLATION) + { + CGlobalTranslationItem* globalTranslationItem = dynamic_cast(resourceFile); + if (globalTranslationItem) + { + globalTranslationItem->Start(); + } + else + { + CScriptDebugging* scriptDebugging = g_pClientGame->GetScriptDebugging(); + SLuaDebugInfo debugInfo; + debugInfo.infoType = DEBUG_INFO_NONE; + debugInfo.strShortSrc = SString("[Resource: %s]", m_strResourceName.c_str()); + + scriptDebugging->LogError(debugInfo, + "Failed to cast resource file to CGlobalTranslationItem"); + } + } } return true; } diff --git a/Client/mods/deathmatch/logic/luadefs/CLuaTranslationDefsClient.cpp b/Client/mods/deathmatch/logic/luadefs/CLuaTranslationDefsClient.cpp index 1bd595458cc..b0f9d6e18db 100644 --- a/Client/mods/deathmatch/logic/luadefs/CLuaTranslationDefsClient.cpp +++ b/Client/mods/deathmatch/logic/luadefs/CLuaTranslationDefsClient.cpp @@ -66,7 +66,7 @@ int CLuaTranslationDefsClient::SetCurrentTranslationLanguage(lua_State* luaVM) } else { - m_pScriptDebugging->LogError(luaVM, "Translation system not initialized for resource '%s'", resource->GetName()); + m_pScriptDebugging->LogWarning(luaVM, "Translation system not initialized for resource '%s'", resource->GetName()); } } } @@ -94,7 +94,7 @@ int CLuaTranslationDefsClient::GetCurrentTranslationLanguage(lua_State* luaVM) } else { - m_pScriptDebugging->LogError(luaVM, "Translation system not initialized for resource '%s'", resource->GetName()); + m_pScriptDebugging->LogWarning(luaVM, "Translation system not initialized for resource '%s'", resource->GetName()); } } } @@ -125,13 +125,15 @@ int CLuaTranslationDefsClient::GetTranslation(lua_State* luaVM) // If no language specified, use current client language std::string targetLanguage = language.empty() ? resource->GetTranslationManager()->GetClientLanguage() : language; + std::string result = resource->GetTranslationManager()->GetTranslation(msgid, targetLanguage); + lua_pushstring(luaVM, result.c_str()); return 1; } else { - m_pScriptDebugging->LogError(luaVM, "Translation system not initialized for resource '%s' when requesting '%s'", resource->GetName(), msgid.c_str()); + m_pScriptDebugging->LogWarning(luaVM, "Translation system not initialized for resource '%s' when requesting '%s'", resource->GetName(), msgid.c_str()); } } } @@ -146,32 +148,30 @@ int CLuaTranslationDefsClient::GetTranslation(lua_State* luaVM) int CLuaTranslationDefsClient::GetAvailableTranslations(lua_State* luaVM) { CLuaMain* luaMain = m_pLuaManager->GetVirtualMachine(luaVM); - if (luaMain) + if (!luaMain) + { + lua_newtable(luaVM); + return 1; + } + + CResource* resource = luaMain->GetResource(); + if (!resource || !resource->GetTranslationManager()) { - CResource* resource = luaMain->GetResource(); if (resource) - { - if (resource->GetTranslationManager()) - { - std::vector languages = resource->GetTranslationManager()->GetAvailableLanguages(); - - lua_newtable(luaVM); - int index = 1; - for (size_t i = 0; i < languages.size(); ++i) - { - lua_pushinteger(luaVM, index++); - lua_pushstring(luaVM, languages[i].c_str()); - lua_settable(luaVM, -3); - } - return 1; - } - else - { - m_pScriptDebugging->LogError(luaVM, "Translation system not initialized for resource '%s'", resource->GetName()); - } - } + m_pScriptDebugging->LogWarning(luaVM, "Translation system not initialized for resource '%s'", resource->GetName()); + + lua_newtable(luaVM); + return 1; } + const std::vector languages = resource->GetTranslationManager()->GetAvailableLanguages(); + lua_newtable(luaVM); + for (size_t i = 0; i < languages.size(); ++i) + { + lua_pushinteger(luaVM, static_cast(i + 1)); + lua_pushstring(luaVM, languages[i].c_str()); + lua_settable(luaVM, -3); + } return 1; } diff --git a/Server/mods/deathmatch/logic/CGlobalTranslationItem.cpp b/Server/mods/deathmatch/logic/CGlobalTranslationItem.cpp new file mode 100644 index 00000000000..3d0ca346ff0 --- /dev/null +++ b/Server/mods/deathmatch/logic/CGlobalTranslationItem.cpp @@ -0,0 +1,61 @@ +/***************************************************************************** +* +* PROJECT: Multi Theft Auto +* LICENSE: See LICENSE in the top level directory +* +* Multi Theft Auto is available from https://www.multitheftauto.com/ +* +*****************************************************************************/ + +#include "StdInc.h" +#include "CGlobalTranslationItem.h" +#include "CResource.h" +#include "CResourceTranslationManager.h" +#include "CScriptDebugging.h" +#include "CGame.h" + +CGlobalTranslationItem::CGlobalTranslationItem(CResource* resource, const char* src, CXMLAttributes* xmlAttributes) + : CResourceFile(resource, src, src, xmlAttributes) +{ + m_type = RESOURCE_FILE_TYPE_GLOBAL_TRANSLATION; + m_providerResourceName = src ? src : ""; +} + +bool CGlobalTranslationItem::Start() +{ + if (m_providerResourceName.empty()) + { + CScriptDebugging* scriptDebugging = g_pGame->GetScriptDebugging(); + SLuaDebugInfo debugInfo; + debugInfo.infoType = DEBUG_INFO_NONE; + debugInfo.strShortSrc = SString("[Resource: %s]", m_resource->GetName().c_str()); + + scriptDebugging->LogError(debugInfo, + "Global translation: Empty provider resource name in meta.xml for resource '%s'. " + "Check your tag.", + m_resource->GetName().c_str()); + return false; + } + + if (m_resource->GetTranslationManager()) + { + m_resource->GetTranslationManager()->AddGlobalTranslationProvider(m_providerResourceName); + return true; + } + + CScriptDebugging* scriptDebugging = g_pGame->GetScriptDebugging(); + SLuaDebugInfo debugInfo; + debugInfo.infoType = DEBUG_INFO_NONE; + debugInfo.strShortSrc = SString("[Resource: %s]", m_resource->GetName().c_str()); + + scriptDebugging->LogError(debugInfo, + "Global translation: Translation manager not available for resource '%s' when adding provider '%s'. " + "This is an internal error - please report this issue.", + m_resource->GetName().c_str(), m_providerResourceName.c_str()); + return false; +} + +bool CGlobalTranslationItem::Stop() +{ + return true; +} \ No newline at end of file diff --git a/Server/mods/deathmatch/logic/CGlobalTranslationItem.h b/Server/mods/deathmatch/logic/CGlobalTranslationItem.h new file mode 100644 index 00000000000..b19878d91ff --- /dev/null +++ b/Server/mods/deathmatch/logic/CGlobalTranslationItem.h @@ -0,0 +1,27 @@ +/***************************************************************************** +* +* PROJECT: Multi Theft Auto +* LICENSE: See LICENSE in the top level directory +* +* Multi Theft Auto is available from https://www.multitheftauto.com/ +* +*****************************************************************************/ + +#pragma once + +#include "CResourceFile.h" +#include + +class CGlobalTranslationItem : public CResourceFile +{ +public: + CGlobalTranslationItem(CResource* resource, const char* src, CXMLAttributes* xmlAttributes); + + std::string GetProviderResourceName() const noexcept { return m_providerResourceName; } + + bool Start() override; + bool Stop() override; + +private: + std::string m_providerResourceName; +}; \ No newline at end of file diff --git a/Server/mods/deathmatch/logic/CResource.cpp b/Server/mods/deathmatch/logic/CResource.cpp index 4f04725c117..9329c6e0dbe 100644 --- a/Server/mods/deathmatch/logic/CResource.cpp +++ b/Server/mods/deathmatch/logic/CResource.cpp @@ -23,7 +23,9 @@ #include "CResourceScriptItem.h" #include "CResourceClientScriptItem.h" #include "CResourceTranslationItem.h" +#include "CGlobalTranslationItem.h" #include "CResourceTranslationManager.h" +#include "CGlobalTranslationManager.h" #include "CAccessControlListManager.h" #include "CScriptDebugging.h" #include "CMapManager.h" @@ -276,7 +278,8 @@ bool CResource::Load() // Read everything that's included. If one of these fail, delete the XML we created and return if (!ReadIncludedResources(pRoot) || !ReadIncludedMaps(pRoot) || !ReadIncludedFiles(pRoot) || !ReadIncludedScripts(pRoot) || - !ReadIncludedHTML(pRoot) || !ReadIncludedExports(pRoot) || !ReadIncludedConfigs(pRoot) || !ReadIncludedTranslations(pRoot)) + !ReadIncludedHTML(pRoot) || !ReadIncludedExports(pRoot) || !ReadIncludedConfigs(pRoot) || !ReadIncludedTranslations(pRoot) || + !ReadIncludedGlobalTranslations(pRoot)) { delete pMetaFile; g_pGame->GetHTTPD()->UnregisterEHS(m_strResourceName.c_str()); @@ -915,7 +918,8 @@ bool CResource::Start(std::list* pDependents, bool bManualStart, con (pResourceFile->GetType() == CResourceFile::RESOURCE_FILE_TYPE_CONFIG && StartOptions.bConfigs) || (pResourceFile->GetType() == CResourceFile::RESOURCE_FILE_TYPE_SCRIPT && StartOptions.bScripts) || (pResourceFile->GetType() == CResourceFile::RESOURCE_FILE_TYPE_CLIENT_SCRIPT && StartOptions.bClientScripts) || - (pResourceFile->GetType() == CResourceFile::RESOURCE_FILE_TYPE_HTML && StartOptions.bHTML)) + (pResourceFile->GetType() == CResourceFile::RESOURCE_FILE_TYPE_HTML && StartOptions.bHTML) || + (pResourceFile->GetType() == CResourceFile::RESOURCE_FILE_TYPE_GLOBAL_TRANSLATION)) { // Start. Failed? if (!pResourceFile->Start()) @@ -1054,6 +1058,12 @@ bool CResource::Start(std::list* pDependents, bool bManualStart, con // Sort by priority, for start grouping on the client m_StartedResources.sort([](CResource* a, CResource* b) { return a->m_iDownloadPriorityGroup > b->m_iDownloadPriorityGroup; }); + // Register as global translation provider if marked as such + if (m_translationManager && m_translationManager->IsGlobalProvider()) + { + CGlobalTranslationManager::GetSingleton().RegisterProvider(m_strResourceName.c_str(), m_translationManager.get()); + } + return true; } @@ -1111,6 +1121,12 @@ bool CResource::Stop(bool bManualStop) // Tell the modules we are stopping g_pGame->GetLuaManager()->GetLuaModuleManager()->ResourceStopping(m_pVM->GetVirtualMachine()); + // Unregister global translation provider if this resource was one + if (m_translationManager && m_translationManager->IsGlobalProvider()) + { + CGlobalTranslationManager::GetSingleton().UnregisterProvider(m_strResourceName.c_str()); + } + // Remove us from the running resources list m_StartedResources.remove(this); @@ -3374,6 +3390,48 @@ bool CResource::ReadIncludedTranslations(CXMLNode* pRoot) return true; } +bool CResource::ReadIncludedGlobalTranslations(CXMLNode* pRoot) +{ + int i = 0; + + // Check for global-translation-provider tag (marks this resource as a provider) + if (pRoot->FindSubNode("global-translation-provider", 0)) + { + if (m_translationManager) + { + m_translationManager->SetAsGlobalProvider(true); + } + } + + // Loop through global-translation nodes (consumer tags) + for (CXMLNode* pGlobalTranslation = pRoot->FindSubNode("global-translation", i); pGlobalTranslation != nullptr; + pGlobalTranslation = pRoot->FindSubNode("global-translation", ++i)) + { + CXMLAttributes& Attributes = pGlobalTranslation->GetAttributes(); + CXMLAttribute* pSrc = Attributes.Find("src"); + + if (pSrc) + { + std::string strProviderResource = pSrc->GetValue(); + + if (!strProviderResource.empty()) + { + m_ResourceFiles.push_back(new CGlobalTranslationItem(this, strProviderResource.c_str(), &Attributes)); + } + else + { + CLogger::LogPrintf("WARNING: Empty 'src' attribute from 'global-translation' node of 'meta.xml' for resource '%s', ignoring\n", m_strResourceName.c_str()); + } + } + else + { + CLogger::LogPrintf("WARNING: Missing 'src' attribute from 'global-translation' node of 'meta.xml' for resource '%s', ignoring\n", m_strResourceName.c_str()); + } + } + + return true; +} + void CResource::SendNoClientCacheScripts(CPlayer* pPlayer) { if (!IsClientScriptsOn()) diff --git a/Server/mods/deathmatch/logic/CResource.h b/Server/mods/deathmatch/logic/CResource.h index 8f332fe5e92..ea6f5a184e0 100644 --- a/Server/mods/deathmatch/logic/CResource.h +++ b/Server/mods/deathmatch/logic/CResource.h @@ -359,6 +359,7 @@ class CResource : public EHS bool ReadIncludedExports(CXMLNode* pRoot); bool ReadIncludedFiles(CXMLNode* pRoot); bool ReadIncludedTranslations(CXMLNode* pRoot); + bool ReadIncludedGlobalTranslations(CXMLNode* pRoot); bool LoadTranslations(); bool CreateVM(bool bEnableOOP); bool DestroyVM(); diff --git a/Server/mods/deathmatch/logic/CResourceFile.h b/Server/mods/deathmatch/logic/CResourceFile.h index a7a2138d033..a9015405c71 100644 --- a/Server/mods/deathmatch/logic/CResourceFile.h +++ b/Server/mods/deathmatch/logic/CResourceFile.h @@ -35,6 +35,7 @@ class CResourceFile RESOURCE_FILE_TYPE_HTML, RESOURCE_FILE_TYPE_CLIENT_FILE, RESOURCE_FILE_TYPE_TRANSLATION, + RESOURCE_FILE_TYPE_GLOBAL_TRANSLATION, RESOURCE_FILE_TYPE_NONE, }; // TODO: sort all client-side enums and use >= (instead of each individual type) on comparisons that use this enum? diff --git a/Server/mods/deathmatch/logic/luadefs/CLuaTranslationDefs.cpp b/Server/mods/deathmatch/logic/luadefs/CLuaTranslationDefs.cpp index 71ef02101aa..e6a87baf221 100644 --- a/Server/mods/deathmatch/logic/luadefs/CLuaTranslationDefs.cpp +++ b/Server/mods/deathmatch/logic/luadefs/CLuaTranslationDefs.cpp @@ -13,6 +13,8 @@ #include "CLuaTranslationDefs.h" #include "CScriptArgReader.h" #include "CResourceTranslationManager.h" +#include "CGlobalTranslationManager.h" +#include "CResource.h" #include "CGame.h" void CLuaTranslationDefs::LoadFunctions() @@ -20,6 +22,9 @@ void CLuaTranslationDefs::LoadFunctions() constexpr static const std::pair functions[]{ {"getTranslation", GetTranslation}, {"getAvailableTranslations", GetAvailableTranslations}, + {"getGlobalTranslationProviders", GetGlobalTranslationProviders}, + {"isResourceGlobalTranslationProvider", IsResourceGlobalTranslationProvider}, + {"getResourceGlobalTranslationProviders", GetResourceGlobalTranslationProviders}, }; for (const auto& [name, func] : functions) @@ -53,7 +58,7 @@ int CLuaTranslationDefs::GetTranslation(lua_State* luaVM) } else { - m_pScriptDebugging->LogError(luaVM, "Translation system not initialized for resource '%s' when requesting '%s'", resource->GetName().c_str(), msgid.c_str()); + m_pScriptDebugging->LogWarning(luaVM, "Translation system not initialized for resource '%s' when requesting '%s'", resource->GetName().c_str(), msgid.c_str()); } } } @@ -68,32 +73,115 @@ int CLuaTranslationDefs::GetTranslation(lua_State* luaVM) int CLuaTranslationDefs::GetAvailableTranslations(lua_State* luaVM) { CLuaMain* luaMain = m_pLuaManager->GetVirtualMachine(luaVM); - if (luaMain) + if (!luaMain) + { + lua_newtable(luaVM); + return 1; + } + + CResource* resource = luaMain->GetResource(); + if (!resource || !resource->GetTranslationManager()) { - CResource* resource = luaMain->GetResource(); if (resource) - { - if (resource->GetTranslationManager()) - { - std::vector languages = resource->GetTranslationManager()->GetAvailableLanguages(); - - lua_newtable(luaVM); - int index = 1; - for (size_t i = 0; i < languages.size(); ++i) - { - lua_pushinteger(luaVM, index++); - lua_pushstring(luaVM, languages[i].c_str()); - lua_settable(luaVM, -3); - } - return 1; - } - else - { - m_pScriptDebugging->LogError(luaVM, "Translation system not initialized for resource '%s'", resource->GetName().c_str()); - } - } + m_pScriptDebugging->LogWarning(luaVM, "Translation system not initialized for resource '%s'", resource->GetName().c_str()); + + lua_newtable(luaVM); + return 1; + } + + const std::vector languages = resource->GetTranslationManager()->GetAvailableLanguages(); + + lua_newtable(luaVM); + for (size_t i = 0; i < languages.size(); ++i) + { + lua_pushinteger(luaVM, static_cast(i + 1)); + lua_pushstring(luaVM, languages[i].c_str()); + lua_settable(luaVM, -3); + } + return 1; +} + +int CLuaTranslationDefs::GetGlobalTranslationProviders(lua_State* luaVM) +{ + const std::vector providers = CGlobalTranslationManager::GetSingleton().GetAvailableProviders(); + + lua_newtable(luaVM); + for (size_t i = 0; i < providers.size(); ++i) + { + lua_pushinteger(luaVM, static_cast(i + 1)); + lua_pushstring(luaVM, providers[i].c_str()); + lua_settable(luaVM, -3); + } + + return 1; +} + +int CLuaTranslationDefs::IsResourceGlobalTranslationProvider(lua_State* luaVM) +{ + CResource* resource; + + CScriptArgReader argStream(luaVM); + argStream.ReadUserData(resource); + + if (argStream.HasErrors()) + { + m_pScriptDebugging->LogCustom(luaVM, argStream.GetFullErrorMessage()); + lua_pushboolean(luaVM, false); + return 1; } + if (!resource || !resource->GetTranslationManager()) + { + m_pScriptDebugging->LogWarning(luaVM, "Translation system not initialized for resource '%s'", resource ? resource->GetName().c_str() : "unknown"); + lua_pushboolean(luaVM, false); + return 1; + } + + const bool isProvider = resource->GetTranslationManager()->IsGlobalProvider(); + lua_pushboolean(luaVM, isProvider); + return 1; +} + +int CLuaTranslationDefs::GetResourceGlobalTranslationProviders(lua_State* luaVM) +{ + CResource* resource = nullptr; + + CScriptArgReader argStream(luaVM); + + if (argStream.NextIsUserData()) + { + argStream.ReadUserData(resource); + } + else + { + CLuaMain* luaMain = m_pLuaManager->GetVirtualMachine(luaVM); + if (luaMain) + resource = luaMain->GetResource(); + } + + if (argStream.HasErrors()) + { + m_pScriptDebugging->LogCustom(luaVM, argStream.GetFullErrorMessage()); + lua_newtable(luaVM); + return 1; + } + + if (!resource || !resource->GetTranslationManager()) + { + m_pScriptDebugging->LogWarning(luaVM, "Translation system not initialized for resource '%s'", resource ? resource->GetName().c_str() : "unknown"); + lua_newtable(luaVM); + return 1; + } + + const std::vector providers = resource->GetTranslationManager()->GetGlobalProviders(); + lua_newtable(luaVM); + for (size_t i = 0; i < providers.size(); ++i) + { + lua_pushinteger(luaVM, static_cast(i + 1)); + lua_pushstring(luaVM, providers[i].c_str()); + lua_settable(luaVM, -3); + } + return 1; } diff --git a/Server/mods/deathmatch/logic/luadefs/CLuaTranslationDefs.h b/Server/mods/deathmatch/logic/luadefs/CLuaTranslationDefs.h index 41e432e2aa5..98759cc3d7e 100644 --- a/Server/mods/deathmatch/logic/luadefs/CLuaTranslationDefs.h +++ b/Server/mods/deathmatch/logic/luadefs/CLuaTranslationDefs.h @@ -20,4 +20,7 @@ class CLuaTranslationDefs : public CLuaDefs private: LUA_DECLARE(GetTranslation); LUA_DECLARE(GetAvailableTranslations); + LUA_DECLARE(GetGlobalTranslationProviders); + LUA_DECLARE(IsResourceGlobalTranslationProvider); + LUA_DECLARE(GetResourceGlobalTranslationProviders); }; diff --git a/Server/mods/deathmatch/logic/packets/CResourceStartPacket.cpp b/Server/mods/deathmatch/logic/packets/CResourceStartPacket.cpp index d2cfacaa88e..9b8ccecb865 100644 --- a/Server/mods/deathmatch/logic/packets/CResourceStartPacket.cpp +++ b/Server/mods/deathmatch/logic/packets/CResourceStartPacket.cpp @@ -15,6 +15,7 @@ #include "CResourceClientFileItem.h" #include "CResourceScriptItem.h" #include "CResourceTranslationItem.h" +#include "CResourceTranslationManager.h" #include "CChecksum.h" #include "CResource.h" #include "CDummy.h" @@ -76,7 +77,8 @@ bool CResourceStartPacket::Write(NetBitStreamInterface& BitStream) const ((*iter)->GetType() == CResourceScriptItem::RESOURCE_FILE_TYPE_CLIENT_SCRIPT && m_pResource->IsClientScriptsOn() && static_cast(*iter)->IsNoClientCache() == false) || ((*iter)->GetType() == CResourceScriptItem::RESOURCE_FILE_TYPE_CLIENT_FILE && m_pResource->IsClientFilesOn()) || - ((*iter)->GetType() == CResourceScriptItem::RESOURCE_FILE_TYPE_TRANSLATION)) + ((*iter)->GetType() == CResourceScriptItem::RESOURCE_FILE_TYPE_TRANSLATION) || + ((*iter)->GetType() == CResourceScriptItem::RESOURCE_FILE_TYPE_GLOBAL_TRANSLATION)) { // Write the Type of chunk to read (F - File, E - Exported Function) BitStream.Write(static_cast('F')); @@ -145,6 +147,10 @@ bool CResourceStartPacket::Write(NetBitStreamInterface& BitStream) const } } + // Write global translation provider flag + bool isGlobalProvider = m_pResource->GetTranslationManager() && m_pResource->GetTranslationManager()->IsGlobalProvider(); + BitStream.WriteBit(isGlobalProvider); + return true; } diff --git a/Shared/mods/deathmatch/logic/CGlobalTranslationManager.cpp b/Shared/mods/deathmatch/logic/CGlobalTranslationManager.cpp new file mode 100644 index 00000000000..b2b161fa2a4 --- /dev/null +++ b/Shared/mods/deathmatch/logic/CGlobalTranslationManager.cpp @@ -0,0 +1,140 @@ +/***************************************************************************** +* +* PROJECT: Multi Theft Auto +* LICENSE: See LICENSE in the top level directory +* +* Multi Theft Auto is available from https://www.multitheftauto.com/ +* +*****************************************************************************/ + +#include "StdInc.h" +#include "CGlobalTranslationManager.h" +#include "CResourceTranslationManager.h" +#include "CLogger.h" +#include "CScriptDebugging.h" +#ifdef MTA_CLIENT + #include "CClientGame.h" +#else + #include "CGame.h" +#endif +#include +#include +#include + +CGlobalTranslationManager& CGlobalTranslationManager::GetSingleton() +{ + static CGlobalTranslationManager instance; + return instance; +} + +bool CGlobalTranslationManager::RegisterProvider(const std::string& resourceName, CResourceTranslationManager* translationManager) +{ + if (resourceName.empty() || !translationManager) + return false; + + std::lock_guard lock(m_providerMutex); + + if (m_providers.find(resourceName) != m_providers.end()) + { + LogWarning("Global translation provider '" + resourceName + "' is already registered, replacing with new instance. " + "This might indicate a resource restart or duplicate provider registration."); + } + + m_providers[resourceName] = translationManager; + return true; +} + +void CGlobalTranslationManager::UnregisterProvider(const std::string& resourceName) +{ + if (resourceName.empty()) + return; + + std::lock_guard lock(m_providerMutex); + + auto it = m_providers.find(resourceName); + if (it != m_providers.end()) + { + m_providers.erase(it); + } +} + +std::string CGlobalTranslationManager::GetGlobalTranslation(const std::vector& providers, const std::string& msgid, const std::string& language) const +{ + if (msgid.empty() || providers.empty()) + return msgid; + + thread_local std::set visitedProviders; + + std::lock_guard lock(m_providerMutex); + + for (const auto& providerName : providers) + { + if (visitedProviders.find(providerName) != visitedProviders.end()) + continue; + + auto it = m_providers.find(providerName); + if (it != m_providers.end() && it->second) + { + visitedProviders.insert(providerName); + + struct VisitGuard { + std::set& visited; + std::string providerName; + VisitGuard(std::set& v, const std::string& name) : visited(v), providerName(name) {} + ~VisitGuard() { visited.erase(providerName); } + } guard(visitedProviders, providerName); + + std::string translation = it->second->GetLocalTranslation(msgid, language); + if (!translation.empty() && translation != msgid) + return translation; + } + } + + return msgid; +} + +std::vector CGlobalTranslationManager::GetAvailableProviders() const +{ + std::lock_guard lock(m_providerMutex); + + std::vector providers; + providers.reserve(m_providers.size()); + + for (const auto& pair : m_providers) + { + providers.push_back(pair.first); + } + + std::sort(providers.begin(), providers.end()); + return providers; +} + +bool CGlobalTranslationManager::IsProviderAvailable(const std::string& resourceName) const +{ + if (resourceName.empty()) + return false; + + std::lock_guard lock(m_providerMutex); + return m_providers.find(resourceName) != m_providers.end(); +} + +void CGlobalTranslationManager::Clear() +{ + std::lock_guard lock(m_providerMutex); + m_providers.clear(); +} + +void CGlobalTranslationManager::LogWarning(const std::string& message) const +{ +#ifdef MTA_CLIENT + CScriptDebugging* pScriptDebugging = g_pClientGame->GetScriptDebugging(); +#else + CScriptDebugging* pScriptDebugging = g_pGame->GetScriptDebugging(); +#endif + + SLuaDebugInfo debugInfo; + debugInfo.infoType = DEBUG_INFO_NONE; + debugInfo.strShortSrc = "[GlobalTranslation]"; + + pScriptDebugging->LogWarning(debugInfo, "%s", message.c_str()); +} \ No newline at end of file diff --git a/Shared/mods/deathmatch/logic/CGlobalTranslationManager.h b/Shared/mods/deathmatch/logic/CGlobalTranslationManager.h new file mode 100644 index 00000000000..1b09189c1ad --- /dev/null +++ b/Shared/mods/deathmatch/logic/CGlobalTranslationManager.h @@ -0,0 +1,44 @@ +/***************************************************************************** +* +* PROJECT: Multi Theft Auto +* LICENSE: See LICENSE in the top level directory +* +* Multi Theft Auto is available from https://www.multitheftauto.com/ +* +*****************************************************************************/ + +#pragma once + +#include +#include +#include +#include + +class CResourceTranslationManager; + +class CGlobalTranslationManager +{ +public: + static CGlobalTranslationManager& GetSingleton(); + + bool RegisterProvider(const std::string& resourceName, CResourceTranslationManager* translationManager); + void UnregisterProvider(const std::string& resourceName); + + std::string GetGlobalTranslation(const std::vector& providers, const std::string& msgid, const std::string& language) const; + std::vector GetAvailableProviders() const; + bool IsProviderAvailable(const std::string& resourceName) const; + + void Clear(); + +private: + CGlobalTranslationManager() = default; + ~CGlobalTranslationManager() = default; + CGlobalTranslationManager(const CGlobalTranslationManager&) = delete; + CGlobalTranslationManager& operator=(const CGlobalTranslationManager&) = delete; + + void LogWarning(const std::string& message) const; + +private: + std::unordered_map m_providers; + mutable std::mutex m_providerMutex; +}; \ No newline at end of file diff --git a/Shared/mods/deathmatch/logic/CResourceTranslationManager.cpp b/Shared/mods/deathmatch/logic/CResourceTranslationManager.cpp index 81242170fcd..a49059a8b88 100644 --- a/Shared/mods/deathmatch/logic/CResourceTranslationManager.cpp +++ b/Shared/mods/deathmatch/logic/CResourceTranslationManager.cpp @@ -9,18 +9,41 @@ #include "StdInc.h" #include "CResourceTranslationManager.h" +#include "CGlobalTranslationManager.h" #include "dictionary_manager.hpp" #include "language.hpp" #include "po_parser.hpp" #include "CLogger.h" +#include "CScriptDebugging.h" +#include "../../../vendor/tinygettext/log.hpp" +#include +#include +#ifdef MTA_CLIENT + #include "CClientGame.h" + #include "CResourceManager.h" + #include "CResource.h" +#else + #include "CGame.h" + #include "CResourceManager.h" + #include "CResource.h" +#endif #include #include #include #include +static bool s_tinyGetTextLoggingInitialized = false; +static std::mutex s_loggingMutex; + CResourceTranslationManager::CResourceTranslationManager(const std::string& resourceName, const std::string& charset) : m_resourceName(resourceName) { + std::lock_guard lock(s_loggingMutex); + if (!s_tinyGetTextLoggingInitialized) + { + SetupTinyGetTextLogging(); + s_tinyGetTextLoggingInitialized = true; + } } CResourceTranslationManager::~CResourceTranslationManager() @@ -28,6 +51,35 @@ CResourceTranslationManager::~CResourceTranslationManager() Clear(); } +bool CResourceTranslationManager::ValidatePoFile(const std::string& filePath) +{ + std::ifstream file(filePath, std::ios::binary | std::ios::ate); + if (!file.is_open()) + { + LogError("Cannot open translation file: " + filePath); + return false; + } + + std::streampos fileSize = file.tellg(); + if (fileSize == 0) + { + LogError("Translation file is empty: " + filePath); + return false; + } + + file.seekg(-1, std::ios::end); + char lastChar; + file.read(&lastChar, 1); + + if (lastChar != '\n') + { + LogError("Translation file must end with a newline: " + filePath); + return false; + } + + return true; +} + bool CResourceTranslationManager::LoadTranslation(const std::string& filePath, bool isPrimary) { std::string language = ExtractLanguageFromPath(filePath); @@ -43,6 +95,9 @@ bool CResourceTranslationManager::LoadTranslation(const std::string& filePath, b return false; } + if (!ValidatePoFile(filePath)) + return false; + tinygettext::Language lang = tinygettext::Language::from_name(language); if (!lang) { @@ -51,7 +106,6 @@ bool CResourceTranslationManager::LoadTranslation(const std::string& filePath, b } try { - // Create a new separate dictionary for each language instead of reusing the same one auto dictionary = std::make_unique(); std::ifstream file(filePath, std::ios::binary); @@ -64,7 +118,7 @@ bool CResourceTranslationManager::LoadTranslation(const std::string& filePath, b tinygettext::POParser::parse(filePath, file, *dictionary); m_translationFiles[language] = filePath; - m_dictionaries[language] = dictionary.release(); // Transfer ownership + m_dictionaries[language] = dictionary.release(); if (isPrimary) { @@ -88,6 +142,17 @@ bool CResourceTranslationManager::LoadTranslation(const std::string& filePath, b } std::string CResourceTranslationManager::GetTranslation(const std::string& msgid, const std::string& language) const +{ + if (msgid.empty()) + return msgid; + + if (!m_globalProviders.empty()) + return GetTranslationWithGlobalFallback(msgid, language); + + return GetLocalTranslation(msgid, language); +} + +std::string CResourceTranslationManager::GetLocalTranslation(const std::string& msgid, const std::string& language) const { if (msgid.empty()) return msgid; @@ -114,10 +179,45 @@ std::string CResourceTranslationManager::GetTranslation(const std::string& msgid if (dictIt == m_dictionaries.end() || !dictIt->second) return msgid; - std::string translation = dictIt->second->translate(msgid); + std::string translation = dictIt->second->translate_silent(msgid); return translation.empty() ? msgid : translation; } +std::string CResourceTranslationManager::GetTranslationWithGlobalFallback(const std::string& msgid, const std::string& language) const +{ + thread_local std::set currentResourceStack; + + if (currentResourceStack.find(m_resourceName) != currentResourceStack.end()) + return msgid; + + currentResourceStack.insert(m_resourceName); + + struct StackGuard { + std::set& stack; + std::string resourceName; + StackGuard(std::set& s, const std::string& r) : stack(s), resourceName(r) {} + ~StackGuard() { stack.erase(resourceName); } + } guard(currentResourceStack, m_resourceName); + + std::string localTranslation = GetLocalTranslation(msgid, language); + if (!localTranslation.empty() && localTranslation != msgid) + return localTranslation; + + std::string globalTranslation = CGlobalTranslationManager::GetSingleton() + .GetGlobalTranslation(m_globalProviders, msgid, language); + if (!globalTranslation.empty() && globalTranslation != msgid) + return globalTranslation; + + if (!language.empty() && language != m_primaryLanguage) + { + std::string primaryTranslation = GetLocalTranslation(msgid, m_primaryLanguage); + if (!primaryTranslation.empty() && primaryTranslation != msgid) + return primaryTranslation; + } + + return msgid; +} + std::vector CResourceTranslationManager::GetAvailableLanguages() const { std::vector languages; @@ -188,17 +288,9 @@ void CResourceTranslationManager::SetClientLanguage(const std::string& language) return; } - LogWarning("SetClientLanguage called with: '" + language + "'"); - LogWarning("Available languages: " + std::to_string(m_translationFiles.size())); - for (const auto& pair : m_translationFiles) - { - LogWarning(" - " + pair.first); - } - if (m_translationFiles.find(language) != m_translationFiles.end()) { m_clientLanguage = language; - LogWarning("Client language set to: '" + m_clientLanguage + "'"); } else { @@ -207,10 +299,6 @@ void CResourceTranslationManager::SetClientLanguage(const std::string& language) { m_clientLanguage = m_primaryLanguage; } - else - { - LogWarning("No primary language set, keeping current: '" + m_clientLanguage + "'"); - } } } @@ -222,7 +310,6 @@ std::string CResourceTranslationManager::GetClientLanguage() const if (!m_primaryLanguage.empty()) return m_primaryLanguage; - // If no primary language, return first available language if (!m_translationFiles.empty()) return m_translationFiles.begin()->first; @@ -233,16 +320,39 @@ void CResourceTranslationManager::Clear() { m_translationFiles.clear(); - // Clean up owned dictionaries for (auto& pair : m_dictionaries) - { delete pair.second; - } - m_dictionaries.clear(); + m_dictionaries.clear(); m_primaryLanguage.clear(); m_clientLanguage.clear(); m_playerLanguages.clear(); + + RemoveAllGlobalProviders(); +} + +void CResourceTranslationManager::AddGlobalTranslationProvider(const std::string& providerResourceName) +{ + if (providerResourceName.empty()) + { + LogError("AddGlobalTranslationProvider called with empty provider name"); + return; + } + + auto it = std::find(m_globalProviders.begin(), m_globalProviders.end(), providerResourceName); + if (it != m_globalProviders.end()) + { + LogWarning("Global translation provider '" + providerResourceName + "' is already registered for this resource. " + "Duplicate tags in meta.xml?"); + return; + } + + m_globalProviders.push_back(providerResourceName); +} + +void CResourceTranslationManager::RemoveAllGlobalProviders() noexcept +{ + m_globalProviders.clear(); } std::string CResourceTranslationManager::ExtractLanguageFromPath(const std::string& filePath) const @@ -251,12 +361,115 @@ std::string CResourceTranslationManager::ExtractLanguageFromPath(const std::stri return path.stem().string(); } +std::string CResourceTranslationManager::ConformTranslationPath(const std::string& message) +{ + std::string result = message; + + size_t resourcesPos = result.find("/resources/"); + if (resourcesPos == std::string::npos) + { + resourcesPos = result.find("\\resources\\"); + if (resourcesPos == std::string::npos) + return result; + } + + size_t startPos = resourcesPos + 11; + if (startPos >= result.length()) + return result; + + std::string resourceRelativePath = result.substr(startPos); + + for (char& c : resourceRelativePath) + { + if (c == '\\') + c = '/'; + } + + return resourceRelativePath; +} + +void CResourceTranslationManager::TinyGetTextErrorCallback(const std::string& message) +{ +#ifdef MTA_CLIENT + CScriptDebugging* pScriptDebugging = g_pClientGame->GetScriptDebugging(); +#else + CScriptDebugging* pScriptDebugging = g_pGame->GetScriptDebugging(); +#endif + + SLuaDebugInfo debugInfo; + debugInfo.infoType = DEBUG_INFO_NONE; + debugInfo.strShortSrc = "[TinyGetText]"; + + std::string cleanMessage = message; + if (!cleanMessage.empty() && cleanMessage.back() == '\n') + cleanMessage.pop_back(); + + cleanMessage = ConformTranslationPath(cleanMessage); + + pScriptDebugging->LogError(debugInfo, "%s", cleanMessage.c_str()); +} + +void CResourceTranslationManager::TinyGetTextWarningCallback(const std::string& message) +{ +#ifdef MTA_CLIENT + CScriptDebugging* pScriptDebugging = g_pClientGame->GetScriptDebugging(); +#else + CScriptDebugging* pScriptDebugging = g_pGame->GetScriptDebugging(); +#endif + + SLuaDebugInfo debugInfo; + debugInfo.infoType = DEBUG_INFO_NONE; + debugInfo.strShortSrc = "[TinyGetText]"; + + std::string cleanMessage = message; + if (!cleanMessage.empty() && cleanMessage.back() == '\n') + cleanMessage.pop_back(); + + cleanMessage = ConformTranslationPath(cleanMessage); + + pScriptDebugging->LogWarning(debugInfo, "%s", cleanMessage.c_str()); +} + +void CResourceTranslationManager::SetupTinyGetTextLogging() +{ + tinygettext::Log::set_log_info_callback(nullptr); + tinygettext::Log::set_log_warning_callback(TinyGetTextWarningCallback); + tinygettext::Log::set_log_error_callback(TinyGetTextErrorCallback); +} + +void CResourceTranslationManager::CleanupTinyGetTextLogging() +{ + tinygettext::Log::set_log_info_callback(nullptr); + tinygettext::Log::set_log_warning_callback(nullptr); + tinygettext::Log::set_log_error_callback(nullptr); +} + void CResourceTranslationManager::LogWarning(const std::string& message) const { - CLogger::LogPrintf("[%s] %s\n", m_resourceName.c_str(), message.c_str()); +#ifdef MTA_CLIENT + CScriptDebugging* pScriptDebugging = g_pClientGame->GetScriptDebugging(); +#else + CScriptDebugging* pScriptDebugging = g_pGame->GetScriptDebugging(); +#endif + + SLuaDebugInfo debugInfo; + debugInfo.infoType = DEBUG_INFO_NONE; + debugInfo.strShortSrc = SString("[Resource: %s]", m_resourceName.c_str()); + + pScriptDebugging->LogWarning(debugInfo, "%s", message.c_str()); } void CResourceTranslationManager::LogError(const std::string& message) const { - CLogger::ErrorPrintf("[%s] %s\n", m_resourceName.c_str(), message.c_str()); +#ifdef MTA_CLIENT + CScriptDebugging* pScriptDebugging = g_pClientGame->GetScriptDebugging(); +#else + CScriptDebugging* pScriptDebugging = g_pGame->GetScriptDebugging(); +#endif + + SLuaDebugInfo debugInfo; + debugInfo.infoType = DEBUG_INFO_NONE; + debugInfo.strShortSrc = SString("[Resource: %s]", m_resourceName.c_str()); + + pScriptDebugging->LogError(debugInfo, "%s", message.c_str()); } diff --git a/Shared/mods/deathmatch/logic/CResourceTranslationManager.h b/Shared/mods/deathmatch/logic/CResourceTranslationManager.h index 94440db02ce..42801b14c6b 100644 --- a/Shared/mods/deathmatch/logic/CResourceTranslationManager.h +++ b/Shared/mods/deathmatch/logic/CResourceTranslationManager.h @@ -40,11 +40,30 @@ class CResourceTranslationManager void Clear(); bool HasTranslations() const noexcept { return !m_translationFiles.empty(); } + + // Global translation support + void AddGlobalTranslationProvider(const std::string& providerResourceName); + std::vector GetGlobalProviders() const noexcept { return m_globalProviders; } + void RemoveAllGlobalProviders() noexcept; + bool IsGlobalProvider() const noexcept { return m_isGlobalProvider; } + void SetAsGlobalProvider(bool isProvider = true) noexcept { m_isGlobalProvider = isProvider; } + + // Enhanced lookup with global fallback + std::string GetLocalTranslation(const std::string& msgid, const std::string& language) const; + std::string GetTranslationWithGlobalFallback(const std::string& msgid, const std::string& language) const; private: std::string ExtractLanguageFromPath(const std::string& filePath) const; + bool ValidatePoFile(const std::string& filePath); void LogWarning(const std::string& message) const; void LogError(const std::string& message) const; + + // TinyGetText logging callbacks + static std::string ConformTranslationPath(const std::string& message); + static void TinyGetTextErrorCallback(const std::string& message); + static void TinyGetTextWarningCallback(const std::string& message); + static void SetupTinyGetTextLogging(); + static void CleanupTinyGetTextLogging(); private: std::string m_primaryLanguage; @@ -54,4 +73,8 @@ class CResourceTranslationManager std::string m_resourceName; std::unordered_map m_playerLanguages; + + // Global translation support + bool m_isGlobalProvider = false; + std::vector m_globalProviders; }; diff --git a/vendor/tinygettext/dictionary.cpp b/vendor/tinygettext/dictionary.cpp index 78518d1ef7e..73c9eebacba 100644 --- a/vendor/tinygettext/dictionary.cpp +++ b/vendor/tinygettext/dictionary.cpp @@ -76,8 +76,20 @@ Dictionary::translate_plural(const std::string& msgid, const std::string& msgid_ return translate_plural(entries, msgid, msgid_plural, num); } +std::string +Dictionary::translate_plural_silent(const std::string& msgid, const std::string& msgid_plural, int num) +{ + return translate_plural_impl(entries, msgid, msgid_plural, num, true); +} + std::string Dictionary::translate_plural(const Entries& dict, const std::string& msgid, const std::string& msgid_plural, int count) +{ + return translate_plural_impl(dict, msgid, msgid_plural, count, false); +} + +std::string +Dictionary::translate_plural_impl(const Entries& dict, const std::string& msgid, const std::string& msgid_plural, int count, bool silent) { Entries::const_iterator i = dict.find(msgid); @@ -94,10 +106,13 @@ Dictionary::translate_plural(const Entries& dict, const std::string& msgid, cons } else { - log_info << "Couldn't translate: " << msgid << std::endl; - log_info << "Candidates: " << std::endl; - for (i = dict.begin(); i != dict.end(); ++i) - log_info << "'" << i->first << "'" << std::endl; + if (!silent) + { + log_info << "Couldn't translate: " << msgid << std::endl; + log_info << "Candidates: " << std::endl; + for (i = dict.begin(); i != dict.end(); ++i) + log_info << "'" << i->first << "'" << std::endl; + } } if (count == 1) // default to english rules @@ -112,8 +127,20 @@ Dictionary::translate(const std::string& msgid) return translate(entries, msgid); } +std::string +Dictionary::translate_silent(const std::string& msgid) +{ + return translate_impl(entries, msgid, true); +} + std::string Dictionary::translate(const Entries& dict, const std::string& msgid) +{ + return translate_impl(dict, msgid, false); +} + +std::string +Dictionary::translate_impl(const Entries& dict, const std::string& msgid, bool silent) { Entries::const_iterator i = dict.find(msgid); if (i != dict.end() && !i->second.empty()) @@ -122,7 +149,10 @@ Dictionary::translate(const Entries& dict, const std::string& msgid) } else { - log_info << "Couldn't translate: " << msgid << std::endl; + if (!silent) + { + log_info << "Couldn't translate: " << msgid << std::endl; + } return msgid; } } diff --git a/vendor/tinygettext/dictionary.hpp b/vendor/tinygettext/dictionary.hpp index bea91cf94b9..f37baf18bcf 100644 --- a/vendor/tinygettext/dictionary.hpp +++ b/vendor/tinygettext/dictionary.hpp @@ -44,7 +44,9 @@ class Dictionary std::map metadata; std::string translate(const Entries& dict, const std::string& msgid); + std::string translate_impl(const Entries& dict, const std::string& msgid, bool silent); std::string translate_plural(const Entries& dict, const std::string& msgid, const std::string& msgidplural, int num); + std::string translate_plural_impl(const Entries& dict, const std::string& msgid, const std::string& msgidplural, int num, bool silent); public: /** Constructs a dictionary converting to the specified \a charset (default UTF-8) */ @@ -67,11 +69,20 @@ class Dictionary /** Translate the string \a msgid. */ std::string translate(const std::string& msgid); + /** Translate the string \a msgid without logging warnings if not found. + Useful for hierarchical translation systems where failed local lookup + doesn't indicate an error. */ + std::string translate_silent(const std::string& msgid); + /** Translate the string \a msgid to its correct plural form, based on the number of items given by \a num. \a msgid_plural is \a msgid in plural form. */ std::string translate_plural(const std::string& msgid, const std::string& msgidplural, int num); + /** Translate the string \a msgid to its correct plural form without logging + warnings if not found. Useful for hierarchical translation systems. */ + std::string translate_plural_silent(const std::string& msgid, const std::string& msgidplural, int num); + /** Translate the string \a msgid that is in context \a msgctx. A context is a way to disambiguate msgids that contain the same letters, but different meaning. For example "exit" might mean to diff --git a/vendor/tinygettext/log_stream.hpp b/vendor/tinygettext/log_stream.hpp index 8ad4c022d67..993b9ab2a74 100644 --- a/vendor/tinygettext/log_stream.hpp +++ b/vendor/tinygettext/log_stream.hpp @@ -25,7 +25,7 @@ namespace tinygettext { // FIXME: very bad to have such things in the API #define log_error if (!Log::log_error_callback); else (Log(Log::log_error_callback)).get() #define log_warning if (!Log::log_warning_callback); else (Log(Log::log_warning_callback)).get() -#define log_info if (!Log::log_info_callback); else (Log(Log::log_warning_callback)).get() +#define log_info if (!Log::log_info_callback); else (Log(Log::log_info_callback)).get() } // namespace tinygettext From 498e7e53795ef37bb89883f082194f2b0251fcfa Mon Sep 17 00:00:00 2001 From: Mohab <133429578+MohabCodeX@users.noreply.github.com> Date: Tue, 26 Aug 2025 06:33:19 +0300 Subject: [PATCH 4/5] Improve language validation, error messages, and fallback handling --- Server/mods/deathmatch/logic/CResource.cpp | 13 +- .../logic/CResourceTranslationManager.cpp | 189 +++++++++++------- .../logic/CResourceTranslationManager.h | 9 +- 3 files changed, 140 insertions(+), 71 deletions(-) diff --git a/Server/mods/deathmatch/logic/CResource.cpp b/Server/mods/deathmatch/logic/CResource.cpp index 9329c6e0dbe..c8affc30ad9 100644 --- a/Server/mods/deathmatch/logic/CResource.cpp +++ b/Server/mods/deathmatch/logic/CResource.cpp @@ -3982,7 +3982,18 @@ bool CResource::LoadTranslations() bool isPrimary = pTranslationItem->IsPrimary(); if (!m_translationManager->LoadTranslation(strFullPath, isPrimary)) { - m_strFailureReason = SString("Failed to load translation file '%s' for resource '%s'", pTranslationItem->GetName(), m_strResourceName.c_str()); + // Use detailed error message from translation manager if available + std::string detailedError = m_translationManager->GetLastError(); + if (!detailedError.empty()) + { + m_strFailureReason = SString("Failed to load translation file '%s' for resource '%s' (%s)", + pTranslationItem->GetName(), m_strResourceName.c_str(), detailedError.c_str()); + } + else + { + m_strFailureReason = SString("Failed to load translation file '%s' for resource '%s'", + pTranslationItem->GetName(), m_strResourceName.c_str()); + } return false; } } diff --git a/Shared/mods/deathmatch/logic/CResourceTranslationManager.cpp b/Shared/mods/deathmatch/logic/CResourceTranslationManager.cpp index a49059a8b88..bf7df16f00e 100644 --- a/Shared/mods/deathmatch/logic/CResourceTranslationManager.cpp +++ b/Shared/mods/deathmatch/logic/CResourceTranslationManager.cpp @@ -55,63 +55,58 @@ bool CResourceTranslationManager::ValidatePoFile(const std::string& filePath) { std::ifstream file(filePath, std::ios::binary | std::ios::ate); if (!file.is_open()) - { - LogError("Cannot open translation file: " + filePath); return false; - } std::streampos fileSize = file.tellg(); if (fileSize == 0) - { - LogError("Translation file is empty: " + filePath); return false; - } file.seekg(-1, std::ios::end); char lastChar; file.read(&lastChar, 1); if (lastChar != '\n') - { - LogError("Translation file must end with a newline: " + filePath); return false; - } return true; } bool CResourceTranslationManager::LoadTranslation(const std::string& filePath, bool isPrimary) { - std::string language = ExtractLanguageFromPath(filePath); - if (language.empty()) + m_lastError.clear(); + + std::string extractedLanguage = ExtractLanguageFromPath(filePath); + if (extractedLanguage.empty()) { - LogError("Could not determine language from file path: " + filePath); + m_lastError = "Could not determine language from file path: " + filePath; return false; } - if (!std::filesystem::exists(filePath)) + std::string validatedLanguage = ValidateLanguageWithCore(extractedLanguage); + if (validatedLanguage.empty()) { - LogError("Translation file not found: " + filePath); + m_lastError = "Invalid language code '" + extractedLanguage + "'"; return false; } + + std::string language = validatedLanguage; - if (!ValidatePoFile(filePath)) - return false; - - tinygettext::Language lang = tinygettext::Language::from_name(language); - if (!lang) + if (!std::filesystem::exists(filePath)) { - LogError("Invalid language code '" + language + "' extracted from: " + filePath); + m_lastError = "Translation file not found: " + filePath; return false; } + if (!ValidatePoFile(filePath)) + return false; + try { auto dictionary = std::make_unique(); std::ifstream file(filePath, std::ios::binary); if (!file.is_open()) { - LogError("Could not open translation file: " + filePath); + m_lastError = "Could not open translation file: " + filePath; return false; } @@ -136,7 +131,7 @@ bool CResourceTranslationManager::LoadTranslation(const std::string& filePath, b } catch (const std::exception& e) { - LogError("Exception loading translation file '" + filePath + "': " + e.what()); + m_lastError = "Exception loading translation file '" + filePath + "': " + e.what(); return false; } } @@ -238,13 +233,40 @@ void CResourceTranslationManager::SetPlayerLanguage(void* player, const std::str if (!player || language.empty()) return; - if (m_translationFiles.find(language) != m_translationFiles.end()) + std::string validatedLanguage = ValidateLanguageWithCore(language); + if (validatedLanguage.empty()) + { +#ifdef MTA_CLIENT + CScriptDebugging* pScriptDebugging = g_pClientGame->GetScriptDebugging(); +#else + CScriptDebugging* pScriptDebugging = g_pGame->GetScriptDebugging(); +#endif + SLuaDebugInfo debugInfo; + debugInfo.infoType = DEBUG_INFO_NONE; + debugInfo.strShortSrc = SString("[Resource: %s]", m_resourceName.c_str()); + + pScriptDebugging->LogError(debugInfo, "Invalid language '%s' - use standard locale format (e.g., en_US, es_ES)", + language.c_str()); + return; + } + + if (m_translationFiles.find(validatedLanguage) != m_translationFiles.end()) { - m_playerLanguages[player] = language; + m_playerLanguages[player] = validatedLanguage; } else { - LogWarning("Language '" + language + "' not available for player, using primary language '" + m_primaryLanguage + "'"); +#ifdef MTA_CLIENT + CScriptDebugging* pScriptDebugging = g_pClientGame->GetScriptDebugging(); +#else + CScriptDebugging* pScriptDebugging = g_pGame->GetScriptDebugging(); +#endif + SLuaDebugInfo debugInfo; + debugInfo.infoType = DEBUG_INFO_NONE; + debugInfo.strShortSrc = SString("[Resource: %s]", m_resourceName.c_str()); + + pScriptDebugging->LogWarning(debugInfo, "Language '%s' not available for player, using primary language '%s'", + validatedLanguage.c_str(), m_primaryLanguage.c_str()); if (!m_primaryLanguage.empty()) m_playerLanguages[player] = m_primaryLanguage; } @@ -256,7 +278,6 @@ std::string CResourceTranslationManager::GetPlayerLanguage(void* player) const { if (!m_primaryLanguage.empty()) return m_primaryLanguage; - // If no primary language, return first available language if (!m_translationFiles.empty()) return m_translationFiles.begin()->first; return ""; @@ -266,7 +287,6 @@ std::string CResourceTranslationManager::GetPlayerLanguage(void* player) const if (it != m_playerLanguages.end()) return it->second; - // Fallback to primary language or first available language if (!m_primaryLanguage.empty()) return m_primaryLanguage; if (!m_translationFiles.empty()) @@ -284,17 +304,53 @@ void CResourceTranslationManager::SetClientLanguage(const std::string& language) { if (language.empty()) { - LogWarning("SetClientLanguage called with empty language"); +#ifdef MTA_CLIENT + CScriptDebugging* pScriptDebugging = g_pClientGame->GetScriptDebugging(); +#else + CScriptDebugging* pScriptDebugging = g_pGame->GetScriptDebugging(); +#endif + SLuaDebugInfo debugInfo; + debugInfo.infoType = DEBUG_INFO_NONE; + debugInfo.strShortSrc = SString("[Resource: %s]", m_resourceName.c_str()); + + pScriptDebugging->LogWarning(debugInfo, "setCurrentTranslationLanguage called with empty language code"); return; } - if (m_translationFiles.find(language) != m_translationFiles.end()) + std::string validatedLanguage = ValidateLanguageWithCore(language); + if (validatedLanguage.empty()) { - m_clientLanguage = language; +#ifdef MTA_CLIENT + CScriptDebugging* pScriptDebugging = g_pClientGame->GetScriptDebugging(); +#else + CScriptDebugging* pScriptDebugging = g_pGame->GetScriptDebugging(); +#endif + SLuaDebugInfo debugInfo; + debugInfo.infoType = DEBUG_INFO_NONE; + debugInfo.strShortSrc = SString("[Resource: %s]", m_resourceName.c_str()); + + pScriptDebugging->LogError(debugInfo, "Invalid language '%s' - use standard locale format (e.g., en_US, es_ES)", + language.c_str()); + return; + } + + if (m_translationFiles.find(validatedLanguage) != m_translationFiles.end()) + { + m_clientLanguage = validatedLanguage; } else { - LogWarning("Language '" + language + "' not found, falling back to primary: '" + m_primaryLanguage + "'"); +#ifdef MTA_CLIENT + CScriptDebugging* pScriptDebugging = g_pClientGame->GetScriptDebugging(); +#else + CScriptDebugging* pScriptDebugging = g_pGame->GetScriptDebugging(); +#endif + SLuaDebugInfo debugInfo; + debugInfo.infoType = DEBUG_INFO_NONE; + debugInfo.strShortSrc = SString("[Resource: %s]", m_resourceName.c_str()); + + pScriptDebugging->LogWarning(debugInfo, "Language '%s' not available, falling back to primary language '%s'", + validatedLanguage.c_str(), m_primaryLanguage.c_str()); if (!m_primaryLanguage.empty()) { m_clientLanguage = m_primaryLanguage; @@ -334,18 +390,11 @@ void CResourceTranslationManager::Clear() void CResourceTranslationManager::AddGlobalTranslationProvider(const std::string& providerResourceName) { if (providerResourceName.empty()) - { - LogError("AddGlobalTranslationProvider called with empty provider name"); return; - } auto it = std::find(m_globalProviders.begin(), m_globalProviders.end(), providerResourceName); if (it != m_globalProviders.end()) - { - LogWarning("Global translation provider '" + providerResourceName + "' is already registered for this resource. " - "Duplicate tags in meta.xml?"); return; - } m_globalProviders.push_back(providerResourceName); } @@ -361,6 +410,40 @@ std::string CResourceTranslationManager::ExtractLanguageFromPath(const std::stri return path.stem().string(); } +std::string CResourceTranslationManager::ValidateLanguageWithCore(const std::string& language) const +{ + if (language.empty()) + return ""; + + std::string normalizedLanguage = language; + + if (normalizedLanguage == "en") + normalizedLanguage = "en_US"; + else if (normalizedLanguage == "fi") + normalizedLanguage = "fi_FI"; + else if (normalizedLanguage == "az") + normalizedLanguage = "az_AZ"; + else if (normalizedLanguage == "ka") + normalizedLanguage = "ka_GE"; + + tinygettext::Language lang = tinygettext::Language::from_name(normalizedLanguage); + if (!lang) + { + lang = tinygettext::Language::from_name("en_US"); + if (!lang) + { + return ""; + } + + return ""; + } + + std::string result = lang.str(); + + return result; +} + + std::string CResourceTranslationManager::ConformTranslationPath(const std::string& message) { std::string result = message; @@ -443,33 +526,3 @@ void CResourceTranslationManager::CleanupTinyGetTextLogging() tinygettext::Log::set_log_warning_callback(nullptr); tinygettext::Log::set_log_error_callback(nullptr); } - -void CResourceTranslationManager::LogWarning(const std::string& message) const -{ -#ifdef MTA_CLIENT - CScriptDebugging* pScriptDebugging = g_pClientGame->GetScriptDebugging(); -#else - CScriptDebugging* pScriptDebugging = g_pGame->GetScriptDebugging(); -#endif - - SLuaDebugInfo debugInfo; - debugInfo.infoType = DEBUG_INFO_NONE; - debugInfo.strShortSrc = SString("[Resource: %s]", m_resourceName.c_str()); - - pScriptDebugging->LogWarning(debugInfo, "%s", message.c_str()); -} - -void CResourceTranslationManager::LogError(const std::string& message) const -{ -#ifdef MTA_CLIENT - CScriptDebugging* pScriptDebugging = g_pClientGame->GetScriptDebugging(); -#else - CScriptDebugging* pScriptDebugging = g_pGame->GetScriptDebugging(); -#endif - - SLuaDebugInfo debugInfo; - debugInfo.infoType = DEBUG_INFO_NONE; - debugInfo.strShortSrc = SString("[Resource: %s]", m_resourceName.c_str()); - - pScriptDebugging->LogError(debugInfo, "%s", message.c_str()); -} diff --git a/Shared/mods/deathmatch/logic/CResourceTranslationManager.h b/Shared/mods/deathmatch/logic/CResourceTranslationManager.h index 42801b14c6b..6fb4bfa02ea 100644 --- a/Shared/mods/deathmatch/logic/CResourceTranslationManager.h +++ b/Shared/mods/deathmatch/logic/CResourceTranslationManager.h @@ -51,12 +51,14 @@ class CResourceTranslationManager // Enhanced lookup with global fallback std::string GetLocalTranslation(const std::string& msgid, const std::string& language) const; std::string GetTranslationWithGlobalFallback(const std::string& msgid, const std::string& language) const; + + // Error handling + std::string GetLastError() const noexcept { return m_lastError; } private: std::string ExtractLanguageFromPath(const std::string& filePath) const; + std::string ValidateLanguageWithCore(const std::string& language) const; bool ValidatePoFile(const std::string& filePath); - void LogWarning(const std::string& message) const; - void LogError(const std::string& message) const; // TinyGetText logging callbacks static std::string ConformTranslationPath(const std::string& message); @@ -77,4 +79,7 @@ class CResourceTranslationManager // Global translation support bool m_isGlobalProvider = false; std::vector m_globalProviders; + + // Error handling + mutable std::string m_lastError; }; From d1d77f6a055f0106bb7db8b9115661c265c6de26 Mon Sep 17 00:00:00 2001 From: Mohab <133429578+MohabCodeX@users.noreply.github.com> Date: Tue, 9 Sep 2025 13:04:26 +0300 Subject: [PATCH 5/5] Fix localization features in CResourceStartPacket --- .../logic/packets/CResourceStartPacket.cpp | 208 +++++++++--------- 1 file changed, 105 insertions(+), 103 deletions(-) diff --git a/Server/mods/deathmatch/logic/packets/CResourceStartPacket.cpp b/Server/mods/deathmatch/logic/packets/CResourceStartPacket.cpp index 1f106cd357c..d3c0acffcac 100644 --- a/Server/mods/deathmatch/logic/packets/CResourceStartPacket.cpp +++ b/Server/mods/deathmatch/logic/packets/CResourceStartPacket.cpp @@ -27,130 +27,132 @@ CResourceStartPacket::CResourceStartPacket(const std::string& resourceName, CRes bool CResourceStartPacket::Write(NetBitStreamInterface& BitStream) const { - if (m_strResourceName.empty()) - return false; - - // Write the resource name - unsigned char sizeResourceName = static_cast(m_strResourceName.size()); - BitStream.Write(sizeResourceName); - if (sizeResourceName > 0) + if (!m_strResourceName.empty()) { - BitStream.Write(m_strResourceName.c_str(), sizeResourceName); - } + // Write the resource name + unsigned char sizeResourceName = static_cast(m_strResourceName.size()); + BitStream.Write(sizeResourceName); + if (sizeResourceName > 0) + { + BitStream.Write(m_strResourceName.c_str(), sizeResourceName); + } - // Write the start counter - BitStream.Write(m_startCounter); + // Write the start counter + BitStream.Write(m_startCounter); - // Write the resource id - BitStream.Write(m_pResource->GetNetID()); + // Write the resource id + BitStream.Write(m_pResource->GetNetID()); - // Write the resource element id - BitStream.Write(m_pResource->GetResourceRootElement()->GetID()); + // Write the resource element id + BitStream.Write(m_pResource->GetResourceRootElement()->GetID()); - // Write the resource dynamic element id - BitStream.Write(m_pResource->GetDynamicElementRoot()->GetID()); + // Write the resource dynamic element id + BitStream.Write(m_pResource->GetDynamicElementRoot()->GetID()); - // Count the amount of 'no client cache' scripts - unsigned short usNoClientCacheScriptCount = 0; - if (m_pResource->IsClientScriptsOn()) - { - for (CResourceFile* resourceFile : m_pResource->GetFiles()) + // Count the amount of 'no client cache' scripts + unsigned short usNoClientCacheScriptCount = 0; + if (m_pResource->IsClientScriptsOn()) { - if (resourceFile->GetType() == CResourceScriptItem::RESOURCE_FILE_TYPE_CLIENT_SCRIPT && - static_cast(resourceFile)->IsNoClientCache()) + for (CResourceFile* resourceFile : m_pResource->GetFiles()) { - ++usNoClientCacheScriptCount; + if (resourceFile->GetType() == CResourceScriptItem::RESOURCE_FILE_TYPE_CLIENT_SCRIPT && + static_cast(resourceFile)->IsNoClientCache()) + { + ++usNoClientCacheScriptCount; + } } } - } - BitStream.Write(usNoClientCacheScriptCount); + BitStream.Write(usNoClientCacheScriptCount); - // Write the declared min client version for this resource - BitStream.WriteString(m_pResource->GetMinServerRequirement()); - BitStream.WriteString(m_pResource->GetMinClientRequirement()); - BitStream.WriteBit(m_pResource->IsOOPEnabledInMetaXml()); - BitStream.Write(m_pResource->GetDownloadPriorityGroup()); + // Write the declared min client version for this resource + BitStream.WriteString(m_pResource->GetMinServerRequirement()); + BitStream.WriteString(m_pResource->GetMinClientRequirement()); + BitStream.WriteBit(m_pResource->IsOOPEnabledInMetaXml()); + BitStream.Write(m_pResource->GetDownloadPriorityGroup()); - // Send the resource files info - for (CResourceFile* resourceFile : m_pResource->GetFiles()) - { - if ((resourceFile->GetType() == CResourceScriptItem::RESOURCE_FILE_TYPE_CLIENT_CONFIG && m_pResource->IsClientConfigsOn()) || - (resourceFile->GetType() == CResourceScriptItem::RESOURCE_FILE_TYPE_CLIENT_SCRIPT && m_pResource->IsClientScriptsOn() && - static_cast(resourceFile)->IsNoClientCache() == false) || - (resourceFile->GetType() == CResourceScriptItem::RESOURCE_FILE_TYPE_CLIENT_FILE && m_pResource->IsClientFilesOn()) || - (resourceFile->GetType() == CResourceScriptItem::RESOURCE_FILE_TYPE_TRANSLATION) || - (resourceFile->GetType() == CResourceScriptItem::RESOURCE_FILE_TYPE_GLOBAL_TRANSLATION)) + // Send the resource files info + for (CResourceFile* resourceFile : m_pResource->GetFiles()) { - // Write the Type of chunk to read (F - File, E - Exported Function) - BitStream.Write(static_cast('F')); - - // Write the map name - const char* szFileName = resourceFile->GetWindowsName(); - size_t sizeFileName = strlen(szFileName); - - // Make sure we don't have any backslashes in the name - char* szCleanedFilename = new char[sizeFileName + 1]; - strcpy(szCleanedFilename, szFileName); - for (unsigned int i = 0; i < sizeFileName; i++) - { - if (szCleanedFilename[i] == '\\') - szCleanedFilename[i] = '/'; - } - - BitStream.Write(static_cast(sizeFileName)); - if (sizeFileName > 0) + if ((resourceFile->GetType() == CResourceScriptItem::RESOURCE_FILE_TYPE_CLIENT_CONFIG && m_pResource->IsClientConfigsOn()) || + (resourceFile->GetType() == CResourceScriptItem::RESOURCE_FILE_TYPE_CLIENT_SCRIPT && m_pResource->IsClientScriptsOn() && + static_cast(resourceFile)->IsNoClientCache() == false) || + (resourceFile->GetType() == CResourceScriptItem::RESOURCE_FILE_TYPE_CLIENT_FILE && m_pResource->IsClientFilesOn()) || + (resourceFile->GetType() == CResourceScriptItem::RESOURCE_FILE_TYPE_TRANSLATION) || + (resourceFile->GetType() == CResourceScriptItem::RESOURCE_FILE_TYPE_GLOBAL_TRANSLATION)) { - BitStream.Write(szCleanedFilename, sizeFileName); - } - - // ChrML: Don't forget this... - delete[] szCleanedFilename; - - BitStream.Write(static_cast(resourceFile->GetType())); - CChecksum checksum = resourceFile->GetLastChecksum(); - BitStream.Write(checksum.ulCRC); - BitStream.Write((const char*)checksum.md5.data, sizeof(checksum.md5.data)); - BitStream.Write((double)resourceFile->GetSizeHint()); // Has to be double for bitstream format compatibility - if (resourceFile->GetType() == CResourceScriptItem::RESOURCE_FILE_TYPE_CLIENT_FILE) - { - CResourceClientFileItem* pRCFItem = reinterpret_cast(resourceFile); - // write bool whether to download or not - BitStream.WriteBit(pRCFItem->IsAutoDownload()); - } - else if (resourceFile->GetType() == CResourceScriptItem::RESOURCE_FILE_TYPE_TRANSLATION) - { - CResourceTranslationItem* translationItem = dynamic_cast(resourceFile); - bool isPrimary = translationItem ? translationItem->IsPrimary() : false; - BitStream.WriteBit(isPrimary); + // Write the Type of chunk to read (F - File, E - Exported Function) + BitStream.Write(static_cast('F')); + + // Write the map name + const char* szFileName = resourceFile->GetWindowsName(); + size_t sizeFileName = strlen(szFileName); + + // Make sure we don't have any backslashes in the name + char* szCleanedFilename = new char[sizeFileName + 1]; + strcpy(szCleanedFilename, szFileName); + for (unsigned int i = 0; i < sizeFileName; i++) + { + if (szCleanedFilename[i] == '\\') + szCleanedFilename[i] = '/'; + } + + BitStream.Write(static_cast(sizeFileName)); + if (sizeFileName > 0) + { + BitStream.Write(szCleanedFilename, sizeFileName); + } + + // ChrML: Don't forget this... + delete[] szCleanedFilename; + + BitStream.Write(static_cast(resourceFile->GetType())); + CChecksum checksum = resourceFile->GetLastChecksum(); + BitStream.Write(checksum.ulCRC); + BitStream.Write((const char*)checksum.md5.data, sizeof(checksum.md5.data)); + BitStream.Write((double)resourceFile->GetSizeHint()); // Has to be double for bitstream format compatibility + if (resourceFile->GetType() == CResourceScriptItem::RESOURCE_FILE_TYPE_CLIENT_FILE) + { + CResourceClientFileItem* pRCFItem = reinterpret_cast(resourceFile); + // write bool whether to download or not + BitStream.WriteBit(pRCFItem->IsAutoDownload()); + } + else if (resourceFile->GetType() == CResourceScriptItem::RESOURCE_FILE_TYPE_TRANSLATION) + { + CResourceTranslationItem* translationItem = dynamic_cast(resourceFile); + bool isPrimary = translationItem ? translationItem->IsPrimary() : false; + BitStream.WriteBit(isPrimary); + } } } - } - // Loop through the exported functions - std::list::iterator iterExportedFunction = m_pResource->IterBeginExportedFunctions(); - for (; iterExportedFunction != m_pResource->IterEndExportedFunctions(); iterExportedFunction++) - { - // Check to see if the exported function is 'client' - if (iterExportedFunction->GetType() == CExportedFunction::EXPORTED_FUNCTION_TYPE_CLIENT) + // Loop through the exported functions + std::list::iterator iterExportedFunction = m_pResource->IterBeginExportedFunctions(); + for (; iterExportedFunction != m_pResource->IterEndExportedFunctions(); iterExportedFunction++) { - // Write the Type of chunk to read (F - File, E - Exported Function) - BitStream.Write(static_cast('E')); - - // Write the exported function - std::string strFunctionName = iterExportedFunction->GetFunctionName(); - size_t sizeFunctionName = strFunctionName.length(); - - BitStream.Write(static_cast(sizeFunctionName)); - if (sizeFunctionName > 0) + // Check to see if the exported function is 'client' + if (iterExportedFunction->GetType() == CExportedFunction::EXPORTED_FUNCTION_TYPE_CLIENT) { - BitStream.Write(strFunctionName.c_str(), sizeFunctionName); + // Write the Type of chunk to read (F - File, E - Exported Function) + BitStream.Write(static_cast('E')); + + // Write the exported function + std::string strFunctionName = iterExportedFunction->GetFunctionName(); + size_t sizeFunctionName = strFunctionName.length(); + + BitStream.Write(static_cast(sizeFunctionName)); + if (sizeFunctionName > 0) + { + BitStream.Write(strFunctionName.c_str(), sizeFunctionName); + } } } - } - // Write global translation provider flag - bool isGlobalProvider = m_pResource->GetTranslationManager() && m_pResource->GetTranslationManager()->IsGlobalProvider(); - BitStream.WriteBit(isGlobalProvider); + // Write global translation provider flag + bool isGlobalProvider = m_pResource->GetTranslationManager() && m_pResource->GetTranslationManager()->IsGlobalProvider(); + BitStream.WriteBit(isGlobalProvider); + + return true; + } - return true; + return false; }