diff --git a/Client/mods/deathmatch/logic/CDownloadableResource.h b/Client/mods/deathmatch/logic/CDownloadableResource.h index f4fe9ee9633..d6e4ddfc8ca 100644 --- a/Client/mods/deathmatch/logic/CDownloadableResource.h +++ b/Client/mods/deathmatch/logic/CDownloadableResource.h @@ -33,6 +33,8 @@ class CDownloadableResource RESOURCE_FILE_TYPE_CLIENT_CONFIG, 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 b43b1cb17b0..c31ce0f3b55 100644 --- a/Client/mods/deathmatch/logic/CPacketHandler.cpp +++ b/Client/mods/deathmatch/logic/CPacketHandler.cpp @@ -22,6 +22,8 @@ #include #include "net/SyncStructures.h" #include "CServerInfo.h" +#include "CResourceTranslationItem.h" +#include "CGlobalTranslationItem.h" using std::list; @@ -5245,6 +5247,29 @@ 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; + } + 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 +5318,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 57a6d61d6c1..a51801c4d99 100644 --- a/Client/mods/deathmatch/logic/CResource.cpp +++ b/Client/mods/deathmatch/logic/CResource.cpp @@ -13,6 +13,9 @@ #define DECLARE_PROFILER_SECTION_CResource #include "profiler/SharedUtil.Profiler.h" #include "CServerIdManager.h" +#include "CResourceTranslationItem.h" +#include "CGlobalTranslationItem.h" +#include "CGlobalTranslationManager.h" using namespace std; @@ -83,6 +86,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 +188,22 @@ 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 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); + } + if (pResourceFile) { m_ResourceFiles.push_back(pResourceFile); @@ -278,6 +298,8 @@ void CResource::Load() } } + LoadTranslations(); + for (auto& list = m_NoClientCacheScriptList; !list.empty(); list.pop_front()) { DECLARE_PROFILER_SECTION(OnPreLoadNoClientCacheScript) @@ -328,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) { @@ -354,6 +382,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); @@ -521,3 +556,43 @@ 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); + } + } + } + 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/CResource.h b/Client/mods/deathmatch/logic/CResource.h index 1791a82c993..2b2a9a94114 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); @@ -154,4 +159,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..b0f9d6e18db --- /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->LogWarning(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->LogWarning(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->LogWarning(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) + { + lua_newtable(luaVM); + return 1; + } + + CResource* resource = luaMain->GetResource(); + if (!resource || !resource->GetTranslationManager()) + { + if (resource) + 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/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 01a5977d1b6..db1530ef4be 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/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 59d0d29f7a4..d96d1746b7f 100644 --- a/Server/mods/deathmatch/logic/CResource.cpp +++ b/Server/mods/deathmatch/logic/CResource.cpp @@ -22,6 +22,10 @@ #include "CResourceClientFileItem.h" #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" @@ -104,6 +108,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; @@ -273,7 +280,16 @@ 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) || + !ReadIncludedGlobalTranslations(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()); @@ -929,7 +945,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()) @@ -1069,6 +1086,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; } @@ -1126,6 +1149,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); @@ -3208,7 +3237,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 } @@ -3350,6 +3380,91 @@ 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; +} + +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()) @@ -3888,3 +4003,33 @@ 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)) + { + // 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; + } + } + } + return true; +} diff --git a/Server/mods/deathmatch/logic/CResource.h b/Server/mods/deathmatch/logic/CResource.h index eeb05f5c67a..5a7a37b0b38 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 @@ -333,6 +334,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; @@ -361,6 +364,9 @@ class CResource : public EHS bool ReadIncludedHTML(CXMLNode* pRoot); bool ReadIncludedExports(CXMLNode* pRoot); bool ReadIncludedFiles(CXMLNode* pRoot); + bool ReadIncludedTranslations(CXMLNode* pRoot); + bool ReadIncludedGlobalTranslations(CXMLNode* pRoot); + bool LoadTranslations(); bool CreateVM(bool bEnableOOP); bool DestroyVM(); void TidyUp(); @@ -457,4 +463,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..a9015405c71 100644 --- a/Server/mods/deathmatch/logic/CResourceFile.h +++ b/Server/mods/deathmatch/logic/CResourceFile.h @@ -34,6 +34,8 @@ class CResourceFile RESOURCE_FILE_TYPE_CLIENT_CONFIG, 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/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 a9c2941bfe5..92a33a89b17 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" #include "CIdArray.h" extern CGame* g_pGame; @@ -246,6 +247,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..e6a87baf221 --- /dev/null +++ b/Server/mods/deathmatch/logic/luadefs/CLuaTranslationDefs.cpp @@ -0,0 +1,187 @@ +/***************************************************************************** + * + * 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 "CGlobalTranslationManager.h" +#include "CResource.h" +#include "CGame.h" + +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) + 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->LogWarning(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) + { + lua_newtable(luaVM); + return 1; + } + + CResource* resource = luaMain->GetResource(); + if (!resource || !resource->GetTranslationManager()) + { + if (resource) + 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 new file mode 100644 index 00000000000..98759cc3d7e --- /dev/null +++ b/Server/mods/deathmatch/logic/luadefs/CLuaTranslationDefs.h @@ -0,0 +1,26 @@ +/***************************************************************************** + * + * 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); + 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 18f9856d74b..d3c0acffcac 100644 --- a/Server/mods/deathmatch/logic/packets/CResourceStartPacket.cpp +++ b/Server/mods/deathmatch/logic/packets/CResourceStartPacket.cpp @@ -14,6 +14,8 @@ #include "CResourceClientScriptItem.h" #include "CResourceClientFileItem.h" #include "CResourceScriptItem.h" +#include "CResourceTranslationItem.h" +#include "CResourceTranslationManager.h" #include "CChecksum.h" #include "CResource.h" #include "CDummy.h" @@ -25,118 +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())) + // 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) - { - 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) + 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)) { - CResourceClientFileItem* pRCFItem = reinterpret_cast(resourceFile); - // write bool whether to download or not - BitStream.WriteBit(pRCFItem->IsAutoDownload()); + // 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); + + return true; } - return true; + return false; } 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/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 new file mode 100644 index 00000000000..bf7df16f00e --- /dev/null +++ b/Shared/mods/deathmatch/logic/CResourceTranslationManager.cpp @@ -0,0 +1,528 @@ +/***************************************************************************** +* +* 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 "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() +{ + Clear(); +} + +bool CResourceTranslationManager::ValidatePoFile(const std::string& filePath) +{ + std::ifstream file(filePath, std::ios::binary | std::ios::ate); + if (!file.is_open()) + return false; + + std::streampos fileSize = file.tellg(); + if (fileSize == 0) + return false; + + file.seekg(-1, std::ios::end); + char lastChar; + file.read(&lastChar, 1); + + if (lastChar != '\n') + return false; + + return true; +} + +bool CResourceTranslationManager::LoadTranslation(const std::string& filePath, bool isPrimary) +{ + m_lastError.clear(); + + std::string extractedLanguage = ExtractLanguageFromPath(filePath); + if (extractedLanguage.empty()) + { + m_lastError = "Could not determine language from file path: " + filePath; + return false; + } + + std::string validatedLanguage = ValidateLanguageWithCore(extractedLanguage); + if (validatedLanguage.empty()) + { + m_lastError = "Invalid language code '" + extractedLanguage + "'"; + return false; + } + + std::string language = validatedLanguage; + + if (!std::filesystem::exists(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()) + { + m_lastError = "Could not open translation file: " + filePath; + return false; + } + + tinygettext::POParser::parse(filePath, file, *dictionary); + + m_translationFiles[language] = filePath; + m_dictionaries[language] = dictionary.release(); + + 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) + { + m_lastError = "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; + + 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; + + 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_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; + 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; + + 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] = validatedLanguage; + } + else + { +#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; + } +} + +std::string CResourceTranslationManager::GetPlayerLanguage(void* player) const +{ + if (!player) + { + if (!m_primaryLanguage.empty()) + return m_primaryLanguage; + if (!m_translationFiles.empty()) + return m_translationFiles.begin()->first; + return ""; + } + + auto it = m_playerLanguages.find(player); + if (it != m_playerLanguages.end()) + return it->second; + + 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()) + { +#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; + } + + 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_clientLanguage = validatedLanguage; + } + else + { +#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; + } + } +} + +std::string CResourceTranslationManager::GetClientLanguage() const +{ + if (!m_clientLanguage.empty()) + return m_clientLanguage; + + if (!m_primaryLanguage.empty()) + return m_primaryLanguage; + + if (!m_translationFiles.empty()) + return m_translationFiles.begin()->first; + + return ""; +} + +void CResourceTranslationManager::Clear() +{ + m_translationFiles.clear(); + + for (auto& pair : m_dictionaries) + delete pair.second; + + m_dictionaries.clear(); + m_primaryLanguage.clear(); + m_clientLanguage.clear(); + m_playerLanguages.clear(); + + RemoveAllGlobalProviders(); +} + +void CResourceTranslationManager::AddGlobalTranslationProvider(const std::string& providerResourceName) +{ + if (providerResourceName.empty()) + return; + + auto it = std::find(m_globalProviders.begin(), m_globalProviders.end(), providerResourceName); + if (it != m_globalProviders.end()) + return; + + m_globalProviders.push_back(providerResourceName); +} + +void CResourceTranslationManager::RemoveAllGlobalProviders() noexcept +{ + m_globalProviders.clear(); +} + +std::string CResourceTranslationManager::ExtractLanguageFromPath(const std::string& filePath) const +{ + std::filesystem::path path(filePath); + 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; + + 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); +} diff --git a/Shared/mods/deathmatch/logic/CResourceTranslationManager.h b/Shared/mods/deathmatch/logic/CResourceTranslationManager.h new file mode 100644 index 00000000000..6fb4bfa02ea --- /dev/null +++ b/Shared/mods/deathmatch/logic/CResourceTranslationManager.h @@ -0,0 +1,85 @@ +/***************************************************************************** +* +* 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(); } + + // 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; + + // 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); + + // 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; + std::string m_clientLanguage; + std::map m_translationFiles; + std::map m_dictionaries; + std::string m_resourceName; + + std::unordered_map m_playerLanguages; + + // Global translation support + bool m_isGlobalProvider = false; + std::vector m_globalProviders; + + // Error handling + mutable std::string m_lastError; +}; diff --git a/premake5.lua b/premake5.lua index ea2d6981eef..51caa6be385 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/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/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/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 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