Core/Chat: Chat translation improvements

* Remove hyperlinks from translated chat messages
* Implement case preservation rules depending on receiver client locale
This commit is contained in:
Shauren
2021-05-03 20:09:39 +02:00
parent 14098b28b3
commit 67e16888ac
7 changed files with 182 additions and 55 deletions

View File

@@ -291,6 +291,24 @@ inline wchar_t wcharToLower(wchar_t wchar)
return wchar;
}
inline bool isLower(wchar_t wchar)
{
if (wchar >= L'a' && wchar <= L'z') // LATIN CAPITAL LETTER A - LATIN CAPITAL LETTER Z
return true;
if (wchar >= 0x00E0 && wchar <= 0x00FF) // LATIN SMALL LETTER A WITH GRAVE - LATIN SMALL LETTER Y WITH DIAERESIS
return true;
if (wchar >= 0x0430 && wchar <= 0x044F) // CYRILLIC SMALL LETTER A - CYRILLIC SMALL LETTER YA
return true;
if (wchar == 0x0451) // CYRILLIC SMALL LETTER IO
return true;
return false;
}
inline bool isUpper(wchar_t wchar)
{
return !isLower(wchar);
}
TC_COMMON_API std::wstring wstrCaseAccentInsensitiveParse(std::wstring const& wstr, LocaleConstant locale);
TC_COMMON_API void wstrToUpper(std::wstring& str);

View File

@@ -106,60 +106,169 @@ LanguageMgr::WordList const* LanguageMgr::FindWordGroup(uint32 language, uint32
return Trinity::Containers::MapGetValuePtr(_wordsMap, WordKey(language, wordLen));
}
std::string LanguageMgr::Translate(std::string const& msg, uint32 sourcePlayerLanguage) const
namespace
{
std::stringstream result;
Tokenizer tokens(msg, ' ');
bool first = true;
void StripHyperlinks(std::string const& source, std::string& dest)
{
dest.resize(source.length());
size_t destSize = 0;
bool skipSquareBrackets = false;
for (size_t i = 0; i < source.length(); ++i)
{
char c = source[i];
if (c != '|')
{
if (!skipSquareBrackets || (c != '[' && c != ']'))
dest[destSize++] = source[i];
continue;
}
if (i + 1 >= source.length())
break;
switch (source[i + 1])
{
case 'c':
case 'C':
// skip color
i += 9;
break;
case 'r':
++i;
break;
case 'H':
// skip just past first |h
i = source.find("|h", i);
if (i != std::string::npos)
i += 2;
skipSquareBrackets = true;
break;
case 'h':
++i;
skipSquareBrackets = false;
break;
case 'T':
// skip just past closing |t
i = source.find("|t", i);
if (i != std::string::npos)
i += 2;
break;
default:
break;
}
}
dest.resize(destSize);
}
void ReplaceUntranslatableCharactersWithSpace(std::string& text)
{
std::wstring wstrText;
if (!Utf8toWStr(text, wstrText))
return;
for (wchar_t& w : wstrText)
if (!isExtendedLatinCharacter(w) && !isNumeric(w) && w <= 0xFF && w != L'\\')
w = L' ';
WStrToUtf8(wstrText, text);
}
static char upper_backslash(char c)
{
return c == '/' ? '\\' : char(toupper(c));
}
static uint32 const sstr_hashtable[16] =
{
0x486E26EE, 0xDCAA16B3, 0xE1918EEF, 0x202DAFDB,
0x341C7DC7, 0x1C365303, 0x40EF2D37, 0x65FD5E49,
0xD6057177, 0x904ECE93, 0x1C38024F, 0x98FD323B,
0xE3061AE7, 0xA39B0FA1, 0x9797F25F, 0xE4444563,
};
uint32 SStrHash(char const* string, bool caseInsensitive, uint32 seed = 0x7FED7FED)
{
ASSERT(string);
uint32 shift = 0xEEEEEEEE;
while (*string)
{
char c = *string++;
if (caseInsensitive)
c = upper_backslash(c);
seed = (sstr_hashtable[c >> 4] - sstr_hashtable[c & 0xF]) ^ (shift + seed);
shift = c + seed + 33 * shift + 3;
}
return seed ? seed : 1;
}
}
std::string LanguageMgr::Translate(std::string const& msg, uint32 language, LocaleConstant locale) const
{
std::string textToTranslate;
StripHyperlinks(msg, textToTranslate);
ReplaceUntranslatableCharactersWithSpace(textToTranslate);
std::string result;
result.reserve(textToTranslate.length());
Tokenizer tokens(textToTranslate, ' ');
for (char const* str : tokens)
{
const char* nextPart = str;
uint32 wordLen = std::min(18U, (uint32)strlen(str));
LanguageMgr::WordList const* wordGroup = FindWordGroup(sourcePlayerLanguage, wordLen);
if (!wordGroup)
nextPart = "";
else
uint32 wordLen = std::min(18u, uint32(strlen(str)));
if (LanguageMgr::WordList const* wordGroup = FindWordGroup(language, wordLen))
{
uint32 wordHash = SStrHash(str, true);
uint8 idxInsideGroup = wordHash % wordGroup->size();
nextPart = wordGroup->at(idxInsideGroup);
char const* replacementWord = (*wordGroup)[idxInsideGroup];
switch (locale)
{
case LOCALE_koKR:
case LOCALE_zhCN:
case LOCALE_zhTW:
{
size_t length = std::min(strlen(str), strlen(replacementWord));
for (size_t i = 0; i < length; ++i)
{
if (str[i] >= 'A' && str[i] <= 'Z')
result += char(toupper(replacementWord[i]));
else
result += replacementWord[i];
}
break;
}
default:
{
std::wstring wstrSourceWord;
if (Utf8toWStr(str, wstrSourceWord))
{
size_t length = std::min(wstrSourceWord.length(), strlen(replacementWord));
for (size_t i = 0; i < length; ++i)
{
if (isUpper(wstrSourceWord[i]))
result += char(toupper(replacementWord[i]));
else
result += char(tolower(replacementWord[i]));
}
}
break;
}
}
}
if (first)
first = false;
else
result << " ";
result << nextPart;
}
return result.str();
}
static char upper_backslash(char c) { return c == '/' ? '\\' : toupper(c); }
static uint32 const s_hashtable[16] = {
0x486E26EE, 0xDCAA16B3, 0xE1918EEF, 0x202DAFDB,
0x341C7DC7, 0x1C365303, 0x40EF2D37, 0x65FD5E49,
0xD6057177, 0x904ECE93, 0x1C38024F, 0x98FD323B,
0xE3061AE7, 0xA39B0FA1, 0x9797F25F, 0xE4444563,
};
uint32 LanguageMgr::SStrHash(char const* string, bool caseInsensitive, uint32 seed) const
{
ASSERT(string);
uint32 shift = 0xEEEEEEEE;
while (*string) {
char c = *string++;
if (caseInsensitive)
c = upper_backslash(c);
seed = (s_hashtable[c >> 4] - s_hashtable[c & 0xF]) ^ (shift + seed);
shift = c + seed + 33 * shift + 3;
result += ' ';
}
return seed ? seed : 1;
if (!result.empty())
result.pop_back();
return result;
}
bool LanguageMgr::IsLanguageExist(uint32 languageId) const

View File

@@ -18,11 +18,10 @@
#ifndef _LANGUAGE_MGR_H
#define _LANGUAGE_MGR_H
#include "Define.h"
#include "Common.h"
#include "Hash.h"
#include "IteratorPair.h"
#include "SharedDefines.h"
#include <string>
#include <vector>
#include <unordered_map>
@@ -60,9 +59,7 @@ class TC_GAME_API LanguageMgr
static LanguageMgr* instance();
//
std::string Translate(std::string const& msg, uint32 sourcePlayerLanguage) const;
uint32 SStrHash(char const* string, bool caseInsensitive, uint32 seed = 0x7FED7FED) const;
std::string Translate(std::string const& msg, uint32 language, LocaleConstant locale) const;
bool IsLanguageExist(uint32 languageId) const;
Trinity::IteratorPair<LanguagesMap::const_iterator> GetLanguageDescById(Language languageId) const;

View File

@@ -21972,13 +21972,14 @@ void Player::Say(std::string const& text, Language language, WorldObject const*
void Player::SendChatMessageToSetInRange(ChatMsg chatMsg, Language language, std::string&& text, float range)
{
Trinity::ChatPacketSender sender(chatMsg, language, this, this, std::move(text));
Trinity::CustomChatTextBuilder builder(this, chatMsg, std::move(text), language, this);
Trinity::LocalizedDo<Trinity::CustomChatTextBuilder> localizer(builder);
// Send to self
sender(this);
localizer(this);
// Send to players
Trinity::MessageDistDeliverer<Trinity::ChatPacketSender> notifier(this, sender, range);
Trinity::MessageDistDeliverer<Trinity::LocalizedDo<Trinity::CustomChatTextBuilder>> notifier(this, localizer, range);
Cell::VisitWorldObjects(this, notifier, range);
}

View File

@@ -1612,7 +1612,7 @@ namespace Trinity
public:
explicit LocalizedDo(Localizer& localizer) : _localizer(localizer) { }
void operator()(Player* p);
void operator()(Player const* p);
private:
Localizer& _localizer;

View File

@@ -750,7 +750,7 @@ void Trinity::PlayerLastSearcher<Check>::Visit(PlayerMapType& m)
}
template<typename Localizer>
void Trinity::LocalizedDo<Localizer>::operator()(Player* p)
void Trinity::LocalizedDo<Localizer>::operator()(Player const* p)
{
LocaleConstant loc_idx = p->GetSession()->GetSessionDbLocaleIndex();
uint32 cache_idx = loc_idx + 1;

View File

@@ -21,6 +21,7 @@
#include "LanguageMgr.h"
#include "ObjectMgr.h"
#include "Player.h"
#include "WorldSession.h"
#include <cstdarg>
namespace Trinity
@@ -44,7 +45,8 @@ void ChatPacketSender::operator()(Player const* player) const
if (!TranslatedPacket)
{
TranslatedPacket.emplace();
TranslatedPacket->Initialize(Type, Language, Sender, Receiver, sLanguageMgr->Translate(Text, Language), AchievementId, "", Locale);
TranslatedPacket->Initialize(Type, Language, Sender, Receiver, sLanguageMgr->Translate(Text, Language, player->GetSession()->GetSessionDbcLocale()),
AchievementId, "", Locale);
TranslatedPacket->Write();
}