Browse Source

Merge pull request #2971 from Nordsoft91/translations

Multi-language support for network game and for VMAPs
Nordsoft91 2 years ago
parent
commit
c57d5545c2
71 changed files with 1168 additions and 426 deletions
  1. 2 2
      client/CPlayerInterface.cpp
  2. 1 1
      client/CServerHandler.cpp
  3. 35 40
      client/lobby/CBonusSelection.cpp
  4. 2 2
      client/lobby/CSelectionBase.cpp
  5. 4 4
      client/lobby/OptionsTab.cpp
  6. 2 2
      client/lobby/RandomMapTab.cpp
  7. 5 5
      client/lobby/SelectionTab.cpp
  8. 1 1
      client/mainmenu/CCampaignScreen.cpp
  9. 1 1
      client/mainmenu/CPrologEpilogVideo.cpp
  10. 1 2
      client/windows/GUIClasses.cpp
  11. 10 9
      lib/CGameInfoCallback.cpp
  12. 89 62
      lib/CGeneralTextHandler.cpp
  13. 67 18
      lib/CGeneralTextHandler.h
  14. 0 1
      lib/IGameCallback.h
  15. 1 1
      lib/MetaString.cpp
  16. 2 2
      lib/StartInfo.cpp
  17. 2 2
      lib/StartInfo.h
  18. 9 9
      lib/campaign/CampaignHandler.cpp
  19. 2 1
      lib/campaign/CampaignScenarioPrologEpilog.h
  20. 5 5
      lib/campaign/CampaignState.cpp
  21. 7 6
      lib/campaign/CampaignState.h
  22. 1 1
      lib/gameState/CGameState.cpp
  23. 2 2
      lib/mapObjects/CGCreature.cpp
  24. 2 1
      lib/mapObjects/CGCreature.h
  25. 6 11
      lib/mapObjects/CGHeroInstance.cpp
  26. 4 4
      lib/mapObjects/CGHeroInstance.h
  27. 4 4
      lib/mapObjects/CGPandoraBox.cpp
  28. 1 1
      lib/mapObjects/CGPandoraBox.h
  29. 10 10
      lib/mapObjects/CGTownInstance.cpp
  30. 4 4
      lib/mapObjects/CGTownInstance.h
  31. 33 30
      lib/mapObjects/CQuest.cpp
  32. 2 1
      lib/mapObjects/CQuest.h
  33. 15 15
      lib/mapObjects/MiscObjects.cpp
  34. 3 3
      lib/mapObjects/MiscObjects.h
  35. 2 2
      lib/mapping/CMap.cpp
  36. 2 1
      lib/mapping/CMap.h
  37. 4 3
      lib/mapping/CMapDefines.h
  38. 79 1
      lib/mapping/CMapHeader.cpp
  39. 20 6
      lib/mapping/CMapHeader.h
  40. 9 9
      lib/mapping/CMapInfo.cpp
  41. 3 3
      lib/mapping/CMapInfo.h
  42. 19 20
      lib/mapping/MapFormatH3M.cpp
  43. 2 1
      lib/mapping/MapFormatH3M.h
  44. 50 13
      lib/mapping/MapFormatJson.cpp
  45. 12 0
      lib/mapping/MapFormatJson.h
  46. 3 1
      lib/modding/CModHandler.cpp
  47. 2 2
      lib/rmg/CMapGenerator.cpp
  48. 3 0
      mapeditor/CMakeLists.txt
  49. BIN
      mapeditor/icons/translations.png
  50. 24 11
      mapeditor/inspector/inspector.cpp
  51. 5 1
      mapeditor/inspector/inspector.h
  52. 4 4
      mapeditor/inspector/rewardswidget.cpp
  53. 4 4
      mapeditor/inspector/rewardswidget.h
  54. 68 30
      mapeditor/mainwindow.cpp
  55. 7 1
      mapeditor/mainwindow.h
  56. 121 17
      mapeditor/mainwindow.ui
  57. 38 14
      mapeditor/mapcontroller.cpp
  58. 2 1
      mapeditor/mapcontroller.h
  59. 4 4
      mapeditor/mapsettings/eventsettings.cpp
  60. 4 4
      mapeditor/mapsettings/generalsettings.cpp
  61. 2 2
      mapeditor/mapsettings/loseconditions.cpp
  62. 2 2
      mapeditor/mapsettings/rumorsettings.cpp
  63. 198 0
      mapeditor/mapsettings/translations.cpp
  64. 45 0
      mapeditor/mapsettings/translations.h
  65. 84 0
      mapeditor/mapsettings/translations.ui
  66. 5 0
      mapeditor/windownewmap.cpp
  67. 2 2
      server/CGameHandler.cpp
  68. 1 1
      server/CVCMIServer.cpp
  69. 1 1
      test/game/CGameStateTest.cpp
  70. 1 1
      test/map/CMapFormatTest.cpp
  71. 1 1
      test/map/MapComparer.cpp

+ 2 - 2
client/CPlayerInterface.cpp

@@ -237,7 +237,7 @@ void CPlayerInterface::performAutosave()
 			prefix = settings["general"]["savePrefix"].String();
 			if(prefix.empty())
 			{
-				std::string name = cb->getMapHeader()->name;
+				std::string name = cb->getMapHeader()->name.toString();
 				int txtlen = TextOperations::getUnicodeCharactersCount(name);
 
 				TextOperations::trimRightUnicode(name, std::max(0, txtlen - 15));
@@ -1716,7 +1716,7 @@ void CPlayerInterface::requestReturningToMainMenu(bool won)
 			if(!ps->checkVanquished())
 				param.allDefeated = false;
 	}
-	param.scenarioName = cb->getMapHeader()->name;
+	param.scenarioName = cb->getMapHeader()->name.toString();
 	param.playerName = cb->getStartInfo()->playerInfos.find(*cb->getPlayerID())->second.name;
 	HighScoreCalculation highScoreCalc;
 	highScoreCalc.parameters.push_back(param);

+ 1 - 1
client/CServerHandler.cpp

@@ -701,7 +701,7 @@ void CServerHandler::startCampaignScenario(HighScoreParameter param, std::shared
 		highScoreCalc->isCampaign = true;
 		highScoreCalc->parameters.clear();
 	}
-	param.campaignName = cs->getName();
+	param.campaignName = cs->getNameTranslated();
 	highScoreCalc->parameters.push_back(param);
 
 	GH.dispatchMainThread([ourCampaign, this]()

+ 35 - 40
client/lobby/CBonusSelection.cpp

@@ -79,9 +79,9 @@ CBonusSelection::CBonusSelection()
 	iconsMapSizes = std::make_shared<CAnimImage>(AnimationPath::builtin("SCNRMPSZ"), 4, 0, 735, 26);
 
 	labelCampaignDescription = std::make_shared<CLabel>(481, 63, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->allTexts[38]);
-	campaignDescription = std::make_shared<CTextBox>(getCampaign()->getDescription(), Rect(480, 86, 286, 117), 1);
+	campaignDescription = std::make_shared<CTextBox>(getCampaign()->getDescriptionTranslated(), Rect(480, 86, 286, 117), 1);
 
-	mapName = std::make_shared<CLabel>(481, 219, FONT_BIG, ETextAlignment::TOPLEFT, Colors::YELLOW, CSH->mi->getName());
+	mapName = std::make_shared<CLabel>(481, 219, FONT_BIG, ETextAlignment::TOPLEFT, Colors::YELLOW, CSH->mi->getNameTranslated());
 	labelMapDescription = std::make_shared<CLabel>(481, 253, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->allTexts[496]);
 	mapDescription = std::make_shared<CTextBox>("", Rect(480, 278, 292, 108), 1);
 
@@ -146,18 +146,18 @@ void CBonusSelection::createBonusesIcons()
 		std::string picName = bonusPics[bonusType];
 		size_t picNumber = bonDescs[i].info2;
 
-		std::string desc;
+		MetaString desc;
 		switch(bonDescs[i].type)
 		{
 		case CampaignBonusType::SPELL:
-			desc = CGI->generaltexth->allTexts[715];
-			boost::algorithm::replace_first(desc, "%s", CGI->spells()->getByIndex(bonDescs[i].info2)->getNameTranslated());
+			desc.appendLocalString(EMetaText::GENERAL_TXT, 715);
+			desc.replaceLocalString(EMetaText::SPELL_NAME, bonDescs[i].info2);
 			break;
 		case CampaignBonusType::MONSTER:
 			picNumber = bonDescs[i].info2 + 2;
-			desc = CGI->generaltexth->allTexts[717];
-			boost::algorithm::replace_first(desc, "%d", std::to_string(bonDescs[i].info3));
-			boost::algorithm::replace_first(desc, "%s", CGI->creatures()->getByIndex(bonDescs[i].info2)->getNamePluralTranslated());
+			desc.appendLocalString(EMetaText::GENERAL_TXT, 717);
+			desc.replaceNumber(bonDescs[i].info3);
+			desc.replaceLocalString(EMetaText::CRE_PL_NAMES, bonDescs[i].info2);
 			break;
 		case CampaignBonusType::BUILDING:
 		{
@@ -182,17 +182,16 @@ void CBonusSelection::createBonusesIcons()
 			picNumber = -1;
 
 			if(vstd::contains((*CGI->townh)[faction]->town->buildings, buildID))
-				desc = (*CGI->townh)[faction]->town->buildings.find(buildID)->second->getNameTranslated();
-
+				desc.appendTextID((*CGI->townh)[faction]->town->buildings.find(buildID)->second->getNameTextID());
 			break;
 		}
 		case CampaignBonusType::ARTIFACT:
-			desc = CGI->generaltexth->allTexts[715];
-			boost::algorithm::replace_first(desc, "%s", CGI->artifacts()->getByIndex(bonDescs[i].info2)->getNameTranslated());
+			desc.appendLocalString(EMetaText::GENERAL_TXT, 715);
+			desc.replaceLocalString(EMetaText::ART_NAMES, bonDescs[i].info2);
 			break;
 		case CampaignBonusType::SPELL_SCROLL:
-			desc = CGI->generaltexth->allTexts[716];
-			boost::algorithm::replace_first(desc, "%s", CGI->spells()->getByIndex(bonDescs[i].info2)->getNameTranslated());
+			desc.appendLocalString(EMetaText::GENERAL_TXT, 716);
+			desc.replaceLocalString(EMetaText::ART_NAMES, bonDescs[i].info2);
 			break;
 		case CampaignBonusType::PRIMARY_SKILL:
 		{
@@ -211,7 +210,7 @@ void CBonusSelection::createBonusesIcons()
 				}
 			}
 			picNumber = leadingSkill;
-			desc = CGI->generaltexth->allTexts[715];
+			desc.appendLocalString(EMetaText::GENERAL_TXT, 715);
 
 			std::string substitute; //text to be printed instead of %s
 			for(int v = 0; v < toPrint.size(); ++v)
@@ -224,14 +223,13 @@ void CBonusSelection::createBonusesIcons()
 				}
 			}
 
-			boost::algorithm::replace_first(desc, "%s", substitute);
+			desc.replaceRawString(substitute);
 			break;
 		}
 		case CampaignBonusType::SECONDARY_SKILL:
-			desc = CGI->generaltexth->allTexts[718];
-
-			boost::algorithm::replace_first(desc, "%s", CGI->generaltexth->levels[bonDescs[i].info3 - 1]); //skill level
-			boost::algorithm::replace_first(desc, "%s", CGI->skillh->getByIndex(bonDescs[i].info2)->getNameTranslated()); //skill name
+			desc.appendLocalString(EMetaText::GENERAL_TXT, 718);
+			desc.replaceTextID(TextIdentifier("core", "genrltxt", "levels", bonDescs[i].info3 - 1).get());
+			desc.replaceLocalString(EMetaText::SEC_SKILL_NAME, bonDescs[i].info2);
 			picNumber = bonDescs[i].info2 * 3 + bonDescs[i].info3 - 1;
 
 			break;
@@ -258,18 +256,17 @@ void CBonusSelection::createBonusesIcons()
 			}
 			picNumber = serialResID;
 
-			desc = CGI->generaltexth->allTexts[717];
-			boost::algorithm::replace_first(desc, "%d", std::to_string(bonDescs[i].info2));
-			std::string replacement;
+			desc.appendLocalString(EMetaText::GENERAL_TXT, 717);
+			desc.replaceNumber(bonDescs[i].info2);
+			
 			if(serialResID <= 6)
 			{
-				replacement = CGI->generaltexth->restypes[serialResID];
+				desc.replaceLocalString(EMetaText::RES_NAMES, serialResID);
 			}
 			else
 			{
-				replacement = CGI->generaltexth->allTexts[714 + serialResID];
+				desc.replaceLocalString(EMetaText::GENERAL_TXT, 714 + serialResID);
 			}
-			boost::algorithm::replace_first(desc, "%s", replacement);
 			break;
 		}
 		case CampaignBonusType::HEROES_FROM_PREVIOUS_SCENARIO:
@@ -278,31 +275,29 @@ void CBonusSelection::createBonusesIcons()
 			if(!superhero)
 				logGlobal->warn("No superhero! How could it be transferred?");
 			picNumber = superhero ? superhero->portrait : 0;
-			desc = CGI->generaltexth->allTexts[719];
-
-			boost::algorithm::replace_first(desc, "%s", getCampaign()->scenario(static_cast<CampaignScenarioID>(bonDescs[i].info2)).scenarioName);
+			desc.appendLocalString(EMetaText::GENERAL_TXT, 719);
+			desc.replaceRawString(getCampaign()->scenario(static_cast<CampaignScenarioID>(bonDescs[i].info2)).scenarioName.toString());
 			break;
 		}
 
 		case CampaignBonusType::HERO:
 
-			desc = CGI->generaltexth->allTexts[718];
-			boost::algorithm::replace_first(desc, "%s", CGI->generaltexth->capColors[bonDescs[i].info1]); //hero's color
-
+			desc.appendLocalString(EMetaText::GENERAL_TXT, 718);
+			desc.replaceTextID(TextIdentifier("core", "genrltxt", "capColors", bonDescs[i].info1).get());
 			if(bonDescs[i].info2 == 0xFFFF)
 			{
-				boost::algorithm::replace_first(desc, "%s", CGI->generaltexth->allTexts[101]); //hero's name
+				desc.replaceLocalString(EMetaText::GENERAL_TXT, 101);
 				picNumber = -1;
 				picName = "CBONN1A3.BMP";
 			}
 			else
 			{
-				boost::algorithm::replace_first(desc, "%s", CGI->heroh->objects[bonDescs[i].info2]->getNameTranslated());
+				desc.replaceTextID(CGI->heroh->objects[bonDescs[i].info2]->getNameTextID());
 			}
 			break;
 		}
 
-		std::shared_ptr<CToggleButton> bonusButton = std::make_shared<CToggleButton>(Point(475 + i * 68, 455), AnimationPath(), CButton::tooltip(desc, desc));
+		std::shared_ptr<CToggleButton> bonusButton = std::make_shared<CToggleButton>(Point(475 + i * 68, 455), AnimationPath(), CButton::tooltip(desc.toString(), desc.toString()));
 
 		if(picNumber != -1)
 			picName += ":" + std::to_string(picNumber);
@@ -355,8 +350,8 @@ void CBonusSelection::updateAfterStateChange()
 	if(!CSH->mi)
 		return;
 	iconsMapSizes->setFrame(CSH->mi->getMapSizeIconId());
-	mapName->setText(CSH->mi->getName());
-	mapDescription->setText(CSH->mi->getDescription());
+	mapName->setText(CSH->mi->getNameTranslated());
+	mapDescription->setText(CSH->mi->getDescriptionTranslated());
 	for(size_t i = 0; i < difficultyIcons.size(); i++)
 	{
 		if(i == CSH->si->difficulty)
@@ -514,9 +509,9 @@ void CBonusSelection::CRegion::clickReleased(const Point & cursorPosition)
 void CBonusSelection::CRegion::showPopupWindow(const Point & cursorPosition)
 {
 	// FIXME: For some reason "down" is only ever contain indeterminate_value
-	auto text = CSH->si->campState->scenario(idOfMapAndRegion).regionText;
-	if(!graphicsNotSelected->getSurface()->isTransparent(cursorPosition - pos.topLeft()) && text.size())
+	auto & text = CSH->si->campState->scenario(idOfMapAndRegion).regionText;
+	if(!graphicsNotSelected->getSurface()->isTransparent(cursorPosition - pos.topLeft()) && !text.empty())
 	{
-		CRClickPopup::createAndPush(text);
+		CRClickPopup::createAndPush(text.toString());
 	}
 }

+ 2 - 2
client/lobby/CSelectionBase.cpp

@@ -200,8 +200,8 @@ void InfoCard::changeSelection()
 		return;
 
 	labelSaveDate->setText(mapInfo->date);
-	mapName->setText(mapInfo->getName());
-	mapDescription->setText(mapInfo->getDescription());
+	mapName->setText(mapInfo->getNameTranslated());
+	mapDescription->setText(mapInfo->getDescriptionTranslated());
 
 	mapDescription->label->scrollTextTo(0, false);
 	if(mapDescription->slider)

+ 4 - 4
client/lobby/OptionsTab.cpp

@@ -405,8 +405,8 @@ std::string OptionsTab::CPlayerSettingsHelper::getName()
 			return CGI->generaltexth->allTexts[522];
 		default:
 		{
-			if(!playerSettings.heroName.empty())
-				return playerSettings.heroName;
+			if(!playerSettings.heroNameTextId.empty())
+				return playerSettings.heroNameTextId;
 			auto index = playerSettings.hero.getNum() >= CGI->heroh->size() ? 0 : playerSettings.hero.getNum();
 			return (*CGI->heroh)[index]->getNameTranslated();
 		}
@@ -927,7 +927,7 @@ void OptionsTab::SelectionWindow::setElement(int elem, bool doApply)
 			if(!doApply)
 			{
 				CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::HERO);
-				if(settings["general"]["enableUiEnhancements"].Bool() && helper.playerSettings.hero.getNum() > PlayerSettings::RANDOM && helper.playerSettings.heroName.empty())
+				if(settings["general"]["enableUiEnhancements"].Bool() && helper.playerSettings.hero.getNum() > PlayerSettings::RANDOM && helper.playerSettings.heroNameTextId.empty())
 					GH.windows().createAndPushWindow<CHeroOverview>(helper.playerSettings.hero);
 				else
 					GH.windows().createAndPushWindow<CPlayerOptionTooltipBox>(helper);
@@ -1013,7 +1013,7 @@ void OptionsTab::SelectedBox::showPopupWindow(const Point & cursorPosition)
 	if(playerSettings.hero.getNum() == PlayerSettings::NONE && !SEL->getPlayerInfo(playerSettings.color.getNum()).hasCustomMainHero() && CPlayerSettingsHelper::type == HERO)
 		return;
 
-	if(settings["general"]["enableUiEnhancements"].Bool() && CPlayerSettingsHelper::type == HERO && playerSettings.hero.getNum() > PlayerSettings::RANDOM && playerSettings.heroName.empty())
+	if(settings["general"]["enableUiEnhancements"].Bool() && CPlayerSettingsHelper::type == HERO && playerSettings.hero.getNum() > PlayerSettings::RANDOM && playerSettings.heroNameTextId.empty())
 		GH.windows().createAndPushWindow<CHeroOverview>(playerSettings.hero);
 	else
 		GH.windows().createAndPushWindow<CPlayerOptionTooltipBox>(*this);

+ 2 - 2
client/lobby/RandomMapTab.cpp

@@ -163,8 +163,8 @@ void RandomMapTab::updateMapInfoByHost()
 	mapInfo->isRandomMap = true;
 	mapInfo->mapHeader = std::make_unique<CMapHeader>();
 	mapInfo->mapHeader->version = EMapFormat::VCMI;
-	mapInfo->mapHeader->name = CGI->generaltexth->allTexts[740];
-	mapInfo->mapHeader->description = CGI->generaltexth->allTexts[741];
+	mapInfo->mapHeader->name.appendLocalString(EMetaText::GENERAL_TXT, 740);
+	mapInfo->mapHeader->description.appendLocalString(EMetaText::GENERAL_TXT, 741);
 	mapInfo->mapHeader->difficulty = 1; // Normal
 	mapInfo->mapHeader->height = mapGenOptions->getHeight();
 	mapInfo->mapHeader->width = mapGenOptions->getWidth();

+ 5 - 5
client/lobby/SelectionTab.cpp

@@ -111,11 +111,11 @@ bool mapSorter::operator()(const std::shared_ptr<ElementInfo> aaa, const std::sh
 			return (a->victoryIconIndex < b->victoryIconIndex);
 			break;
 		case _name: //by name
-			return boost::ilexicographical_compare(a->name, b->name);
+			return boost::ilexicographical_compare(a->name.toString(), b->name.toString());
 		case _fileName: //by filename
 			return boost::ilexicographical_compare(aaa->fileURI, bbb->fileURI);
 		default:
-			return boost::ilexicographical_compare(a->name, b->name);
+			return boost::ilexicographical_compare(a->name.toString(), b->name.toString());
 		}
 	}
 	else //if we are sorting campaigns
@@ -125,9 +125,9 @@ bool mapSorter::operator()(const std::shared_ptr<ElementInfo> aaa, const std::sh
 		case _numOfMaps: //by number of maps in campaign
 			return aaa->campaign->scenariosCount() < bbb->campaign->scenariosCount();
 		case _name: //by name
-			return boost::ilexicographical_compare(aaa->campaign->getName(), bbb->campaign->getName());
+			return boost::ilexicographical_compare(aaa->campaign->getNameTranslated(), bbb->campaign->getNameTranslated());
 		default:
-			return boost::ilexicographical_compare(aaa->campaign->getName(), bbb->campaign->getName());
+			return boost::ilexicographical_compare(aaa->campaign->getNameTranslated(), bbb->campaign->getNameTranslated());
 		}
 	}
 }
@@ -367,7 +367,7 @@ void SelectionTab::showPopupWindow(const Point & cursorPosition)
 
 	if(!curItems[py]->isFolder)
 	{
-		std::string text = boost::str(boost::format("{%1%}\r\n\r\n%2%:\r\n%3%") % curItems[py]->getName() % CGI->generaltexth->translate("vcmi.lobby.filename") % curItems[py]->fullFileURI);
+		std::string text = boost::str(boost::format("{%1%}\r\n\r\n%2%:\r\n%3%") % curItems[py]->getNameTranslated() % CGI->generaltexth->translate("vcmi.lobby.filename") % curItems[py]->fullFileURI);
 		if(curItems[py]->date != "")
 			text += boost::str(boost::format("\r\n\r\n%1%:\r\n%2%") % CGI->generaltexth->translate("vcmi.lobby.creationDate") % curItems[py]->date);
 

+ 1 - 1
client/mainmenu/CCampaignScreen.cpp

@@ -105,7 +105,7 @@ CCampaignScreen::CCampaignButton::CCampaignButton(const JsonNode & config, const
 	status = CCampaignScreen::ENABLED;
 
 	auto header = CampaignHandler::getHeader(campFile);
-	hoverText = header->getName();
+	hoverText = header->getNameTranslated();
 
 	if(persistentStorage["completedCampaigns"][header->getFilename()].Bool())
 		status = CCampaignScreen::COMPLETED;

+ 1 - 1
client/mainmenu/CPrologEpilogVideo.cpp

@@ -36,7 +36,7 @@ CPrologEpilogVideo::CPrologEpilogVideo(CampaignScenarioPrologEpilog _spe, std::f
 	};
 	CCS->soundh->setCallback(voiceSoundHandle, onVoiceStop);
 
-	text = std::make_shared<CMultiLineLabel>(Rect(100, 500, 600, 100), EFonts::FONT_BIG, ETextAlignment::CENTER, Colors::METALLIC_GOLD, spe.prologText);
+	text = std::make_shared<CMultiLineLabel>(Rect(100, 500, 600, 100), EFonts::FONT_BIG, ETextAlignment::CENTER, Colors::METALLIC_GOLD, spe.prologText.toString());
 	text->scrollTextTo(-100);
 }
 

+ 1 - 2
client/windows/GUIClasses.cpp

@@ -456,8 +456,7 @@ CTavernWindow::CTavernWindow(const CGObjectInstance * TavernObj)
 	heroDescription = std::make_shared<CTextBox>("", Rect(30, 373, 233, 35), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE);
 	heroesForHire = std::make_shared<CLabel>(145, 283, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->jktexts[38]);
 
-	auto rumorText = boost::str(boost::format(CGI->generaltexth->allTexts[216]) % LOCPLINT->cb->getTavernRumor(tavernObj));
-	rumor = std::make_shared<CTextBox>(rumorText, Rect(32, 188, 330, 66), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE);
+	rumor = std::make_shared<CTextBox>(LOCPLINT->cb->getTavernRumor(tavernObj), Rect(32, 188, 330, 66), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE);
 
 	statusbar = CGStatusBar::create(std::make_shared<CPicture>(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26));
 	cancel = std::make_shared<CButton>(Point(310, 428), AnimationPath::builtin("ICANCEL.DEF"), CButton::tooltip(CGI->generaltexth->tavernInfo[7]), std::bind(&CTavernWindow::close, this), EShortcut::GLOBAL_CANCEL);

+ 10 - 9
lib/CGameInfoCallback.cpp

@@ -649,33 +649,34 @@ EPlayerStatus CGameInfoCallback::getPlayerStatus(PlayerColor player, bool verbos
 
 std::string CGameInfoCallback::getTavernRumor(const CGObjectInstance * townOrTavern) const
 {
-	std::string text;
+	MetaString text;
+	text.appendLocalString(EMetaText::GENERAL_TXT, 216);
+	
 	std::string extraText;
 	if(gs->rumor.type == RumorState::TYPE_NONE)
-		return text;
+		return text.toString();
 
 	auto rumor = gs->rumor.last[gs->rumor.type];
 	switch(gs->rumor.type)
 	{
 	case RumorState::TYPE_SPECIAL:
+		text.replaceLocalString(EMetaText::GENERAL_TXT, rumor.first);
 		if(rumor.first == RumorState::RUMOR_GRAIL)
-			extraText = VLC->generaltexth->arraytxt[158 + rumor.second];
+			text.replaceTextID(TextIdentifier("core", "genrltxt", "arraytxt", 158 + rumor.second).get());
 		else
-			extraText = VLC->generaltexth->capColors[rumor.second];
-
-		text = boost::str(boost::format(VLC->generaltexth->allTexts[rumor.first]) % extraText);
+			text.replaceTextID(TextIdentifier("core", "genrltxt", "capitalColors", rumor.second).get());
 
 		break;
 	case RumorState::TYPE_MAP:
-		text = gs->map->rumors[rumor.first].text;
+		text.replaceRawString(gs->map->rumors[rumor.first].text.toString());
 		break;
 
 	case RumorState::TYPE_RAND:
-		text = VLC->generaltexth->tavernRumors[rumor.first];
+		text.replaceTextID(TextIdentifier("core", "genrltxt", "randtvrn", rumor.first).get());
 		break;
 	}
 
-	return text;
+	return text.toString();
 }
 
 PlayerRelations CGameInfoCallback::getPlayerRelations( PlayerColor color1, PlayerColor color2 ) const

+ 89 - 62
lib/CGeneralTextHandler.cpp

@@ -11,6 +11,7 @@
 #include "CGeneralTextHandler.h"
 
 #include "filesystem/Filesystem.h"
+#include "serializer/JsonSerializeFormat.h"
 #include "CConfigHandler.h"
 #include "GameSettings.h"
 #include "mapObjects/CQuest.h"
@@ -247,22 +248,38 @@ bool CLegacyConfigParser::endLine()
 	return curr < end;
 }
 
-void CGeneralTextHandler::readToVector(const std::string & sourceID, const std::string & sourceName)
+void TextLocalizationContainer::registerStringOverride(const std::string & modContext, const std::string & language, const TextIdentifier & UID, const std::string & localized)
 {
-	CLegacyConfigParser parser(TextPath::builtin(sourceName));
-	size_t index = 0;
-	do
-	{
-		registerString( "core", {sourceID, index}, parser.readString());
-		index += 1;
-	}
-	while (parser.endLine());
+	assert(!modContext.empty());
+	assert(!language.empty());
+
+	// NOTE: implicitly creates entry, intended - strings added by maps, campaigns, vcmi and potentially - UI mods are not registered anywhere at the moment
+	auto & entry = stringsLocalizations[UID.get()];
+
+	entry.overrideLanguage = language;
+	entry.overrideValue = localized;
+	if (entry.modContext.empty())
+		entry.modContext = modContext;
+}
+
+void TextLocalizationContainer::addSubContainer(const TextLocalizationContainer & container)
+{
+	subContainers.insert(&container);
+}
+
+void TextLocalizationContainer::removeSubContainer(const TextLocalizationContainer & container)
+{
+	subContainers.erase(&container);
 }
 
-const std::string & CGeneralTextHandler::deserialize(const TextIdentifier & identifier) const
+const std::string & TextLocalizationContainer::deserialize(const TextIdentifier & identifier) const
 {
 	if(stringsLocalizations.count(identifier.get()) == 0)
 	{
+		for(const auto * container : subContainers)
+			if(container->identifierExists(identifier))
+				return container->deserialize(identifier);
+		
 		logGlobal->error("Unable to find localization for string '%s'", identifier.get());
 		return identifier.get();
 	}
@@ -274,54 +291,37 @@ const std::string & CGeneralTextHandler::deserialize(const TextIdentifier & iden
 	return entry.baseValue;
 }
 
-void CGeneralTextHandler::registerString(const std::string & modContext, const TextIdentifier & UID, const std::string & localized)
+void TextLocalizationContainer::registerString(const std::string & modContext, const TextIdentifier & UID, const std::string & localized, const std::string & language)
 {
 	assert(!modContext.empty());
-	assert(!getModLanguage(modContext).empty());
+	assert(!Languages::getLanguageOptions(language).identifier.empty());
 	assert(UID.get().find("..") == std::string::npos); // invalid identifier - there is section that was evaluated to empty string
 	//assert(stringsLocalizations.count(UID.get()) == 0); // registering already registered string?
 
 	if(stringsLocalizations.count(UID.get()) > 0)
 	{
 		auto & value = stringsLocalizations[UID.get()];
-
-		if(value.baseLanguage.empty())
-		{
-			value.baseLanguage = getModLanguage(modContext);
-			value.baseValue = localized;
-		}
-		else
-		{
-			if(value.baseValue != localized)
-				logMod->warn("Duplicate registered string '%s' found! Old value: '%s', new value: '%s'", UID.get(), value.baseValue, localized);
-		}
+		value.baseLanguage = language;
+		value.baseValue = localized;
 	}
 	else
 	{
-		StringState result;
-		result.baseLanguage = getModLanguage(modContext);
-		result.baseValue = localized;
-		result.modContext = modContext;
+		StringState value;
+		value.baseLanguage = language;
+		value.baseValue = localized;
+		value.modContext = modContext;
 
-		stringsLocalizations[UID.get()] = result;
+		stringsLocalizations[UID.get()] = value;
 	}
 }
 
-void CGeneralTextHandler::registerStringOverride(const std::string & modContext, const std::string & language, const TextIdentifier & UID, const std::string & localized)
+void TextLocalizationContainer::registerString(const std::string & modContext, const TextIdentifier & UID, const std::string & localized)
 {
-	assert(!modContext.empty());
-	assert(!language.empty());
-
-	// NOTE: implicitly creates entry, intended - strings added by maps, campaigns, vcmi and potentially - UI mods are not registered anywhere at the moment
-	auto & entry = stringsLocalizations[UID.get()];
-
-	entry.overrideLanguage = language;
-	entry.overrideValue = localized;
-	if (entry.modContext.empty())
-		entry.modContext = modContext;
+	assert(!getModLanguage(modContext).empty());
+	registerString(modContext, UID, localized, getModLanguage(modContext));
 }
 
-bool CGeneralTextHandler::validateTranslation(const std::string & language, const std::string & modContext, const JsonNode & config) const
+bool TextLocalizationContainer::validateTranslation(const std::string & language, const std::string & modContext, const JsonNode & config) const
 {
 	bool allPresent = true;
 
@@ -372,12 +372,60 @@ bool CGeneralTextHandler::validateTranslation(const std::string & language, cons
 	return allPresent && allFound;
 }
 
-void CGeneralTextHandler::loadTranslationOverrides(const std::string & language, const std::string & modContext, const JsonNode & config)
+void TextLocalizationContainer::loadTranslationOverrides(const std::string & language, const std::string & modContext, const JsonNode & config)
 {
 	for(const auto & node : config.Struct())
 		registerStringOverride(modContext, language, node.first, node.second.String());
 }
 
+bool TextLocalizationContainer::identifierExists(const TextIdentifier & UID) const
+{
+	return stringsLocalizations.count(UID.get());
+}
+
+void TextLocalizationContainer::dumpAllTexts()
+{
+	logGlobal->info("BEGIN TEXT EXPORT");
+	for(const auto & entry : stringsLocalizations)
+	{
+		if (!entry.second.overrideValue.empty())
+			logGlobal->info(R"("%s" : "%s",)", entry.first, TextOperations::escapeString(entry.second.overrideValue));
+		else
+			logGlobal->info(R"("%s" : "%s",)", entry.first, TextOperations::escapeString(entry.second.baseValue));
+	}
+
+	logGlobal->info("END TEXT EXPORT");
+}
+
+std::string TextLocalizationContainer::getModLanguage(const std::string & modContext)
+{
+	if (modContext == "core")
+		return CGeneralTextHandler::getInstalledLanguage();
+	return VLC->modh->getModLanguage(modContext);
+}
+
+void TextLocalizationContainer::jsonSerialize(JsonNode & dest) const
+{
+	for(auto & s : stringsLocalizations)
+	{
+		dest.Struct()[s.first].String() = s.second.baseValue;
+		if(!s.second.overrideValue.empty())
+			dest.Struct()[s.first].String() = s.second.overrideValue;
+	}
+}
+
+void CGeneralTextHandler::readToVector(const std::string & sourceID, const std::string & sourceName)
+{
+	CLegacyConfigParser parser(TextPath::builtin(sourceName));
+	size_t index = 0;
+	do
+	{
+		registerString( "core", {sourceID, index}, parser.readString());
+		index += 1;
+	}
+	while (parser.endLine());
+}
+
 CGeneralTextHandler::CGeneralTextHandler():
 	victoryConditions(*this, "core.vcdesc"   ),
 	lossCondtions    (*this, "core.lcdesc"   ),
@@ -591,20 +639,6 @@ int32_t CGeneralTextHandler::pluralText(const int32_t textIndex, const int32_t c
 	return textIndex + 1;
 }
 
-void CGeneralTextHandler::dumpAllTexts()
-{
-	logGlobal->info("BEGIN TEXT EXPORT");
-	for(const auto & entry : stringsLocalizations)
-	{
-		if (!entry.second.overrideValue.empty())
-			logGlobal->info(R"("%s" : "%s",)", entry.first, TextOperations::escapeString(entry.second.overrideValue));
-		else
-			logGlobal->info(R"("%s" : "%s",)", entry.first, TextOperations::escapeString(entry.second.baseValue));
-	}
-
-	logGlobal->info("END TEXT EXPORT");
-}
-
 size_t CGeneralTextHandler::getCampaignLength(size_t campaignID) const
 {
 	assert(campaignID < scenariosCountPerCampaign.size());
@@ -614,13 +648,6 @@ size_t CGeneralTextHandler::getCampaignLength(size_t campaignID) const
 	return 0;
 }
 
-std::string CGeneralTextHandler::getModLanguage(const std::string & modContext)
-{
-	if (modContext == "core")
-		return getInstalledLanguage();
-	return VLC->modh->getModLanguage(modContext);
-}
-
 std::string CGeneralTextHandler::getPreferredLanguage()
 {
 	assert(!settings["general"]["language"].String().empty());

+ 67 - 18
lib/CGeneralTextHandler.h

@@ -15,6 +15,7 @@ VCMI_LIB_NAMESPACE_BEGIN
 
 class CInputStream;
 class JsonNode;
+class JsonSerializeFormat;
 
 /// Parser for any text files from H3
 class DLL_LINKAGE CLegacyConfigParser
@@ -113,9 +114,9 @@ public:
 	{}
 };
 
-/// Handles all text-related data in game
-class DLL_LINKAGE CGeneralTextHandler
+class DLL_LINKAGE TextLocalizationContainer
 {
+protected:
 	struct StringState
 	{
 		/// Human-readable string that was added on registration
@@ -132,21 +133,26 @@ class DLL_LINKAGE CGeneralTextHandler
 
 		/// ID of mod that created this string
 		std::string modContext;
+		
+		template <typename Handler>
+		void serialize(Handler & h, const int Version)
+		{
+			h & baseValue;
+			h & baseLanguage;
+			h & modContext;
+		}
 	};
-
+	
 	/// map identifier -> localization
 	std::unordered_map<std::string, StringState> stringsLocalizations;
-
-	void readToVector(const std::string & sourceID, const std::string & sourceName);
-
-	/// number of scenarios in specific campaign. TODO: move to a better location
-	std::vector<size_t> scenariosCountPerCampaign;
-
-	std::string getModLanguage(const std::string & modContext);
-
+	
+	std::set<const TextLocalizationContainer *> subContainers;
+	
 	/// add selected string to internal storage as high-priority strings
 	void registerStringOverride(const std::string & modContext, const std::string & language, const TextIdentifier & UID, const std::string & localized);
-
+	
+	std::string getModLanguage(const std::string & modContext);
+	
 public:
 	/// validates translation of specified language for specified mod
 	/// returns true if localization is valid and complete
@@ -157,13 +163,13 @@ public:
 	/// Any entries loaded by this will have priority over texts registered normally
 	void loadTranslationOverrides(const std::string & language, const std::string & modContext, JsonNode const & file);
 
+	// returns true if identifier with such name was registered, even if not translated to current language
+	bool identifierExists(const TextIdentifier & UID) const;
+	
 	/// add selected string to internal storage
 	void registerString(const std::string & modContext, const TextIdentifier & UID, const std::string & localized);
-
-	// returns true if identifier with such name was registered, even if not translated to current language
-	// not required right now, can be added if necessary
-	// bool identifierExists( const std::string identifier) const;
-
+	void registerString(const std::string & modContext, const TextIdentifier & UID, const std::string & localized, const std::string & language);
+	
 	/// returns translated version of a string that can be displayed to user
 	template<typename  ... Args>
 	std::string translate(std::string arg1, Args ... args) const
@@ -174,10 +180,53 @@ public:
 
 	/// converts identifier into user-readable string
 	const std::string & deserialize(const TextIdentifier & identifier) const;
-
+	
 	/// Debug method, dumps all currently known texts into console using Json-like format
 	void dumpAllTexts();
+	
+	/// Add or override subcontainer which can store identifiers
+	void addSubContainer(const TextLocalizationContainer & container);
+	
+	/// Remove subcontainer with give name
+	void removeSubContainer(const TextLocalizationContainer & container);
+	
+	void jsonSerialize(JsonNode & dest) const;
+	
+	template <typename Handler>
+	void serialize(Handler & h, const int Version)
+	{
+		std::string key;
+		auto sz = stringsLocalizations.size();
+		h & sz;
+		if(h.saving)
+		{
+			for(auto s : stringsLocalizations)
+			{
+				key = s.first;
+				h & key;
+				h & s.second;
+			}
+		}
+		else
+		{
+			for(size_t i = 0; i < sz; ++i)
+			{
+				h & key;
+				h & stringsLocalizations[key];
+			}
+		}
+	}
+};
+
+/// Handles all text-related data in game
+class DLL_LINKAGE CGeneralTextHandler: public TextLocalizationContainer
+{
+	void readToVector(const std::string & sourceID, const std::string & sourceName);
 
+	/// number of scenarios in specific campaign. TODO: move to a better location
+	std::vector<size_t> scenariosCountPerCampaign;
+
+public:
 	LegacyTextContainer allTexts;
 
 	LegacyTextContainer arraytxt;

+ 0 - 1
lib/IGameCallback.h

@@ -20,7 +20,6 @@ struct SetMovePoints;
 struct GiveBonus;
 struct BlockingDialog;
 struct TeleportDialog;
-class MetaString;
 struct StackLocation;
 struct ArtifactLocation;
 class CCreatureSet;

+ 1 - 1
lib/MetaString.cpp

@@ -102,7 +102,7 @@ void MetaString::clear()
 
 bool MetaString::empty() const
 {
-	return message.empty();
+	return message.empty() || toString().empty();
 }
 
 std::string MetaString::getLocalString(const std::pair<EMetaText, ui32> & txt) const

+ 2 - 2
lib/StartInfo.cpp

@@ -62,8 +62,8 @@ PlayerSettings * StartInfo::getPlayersSettings(const ui8 connectedPlayerId)
 
 std::string StartInfo::getCampaignName() const
 {
-	if(!campState->getName().empty())
-		return campState->getName();
+	if(!campState->getNameTranslated().empty())
+		return campState->getNameTranslated();
 	else
 		return VLC->generaltexth->allTexts[508];
 }

+ 2 - 2
lib/StartInfo.h

@@ -59,7 +59,7 @@ struct DLL_LINKAGE PlayerSettings
 	HeroTypeID hero;
 	HeroTypeID heroPortrait; //-1 if default, else ID
 
-	std::string heroName;
+	std::string heroNameTextId;
 	PlayerColor color; //from 0 -
 	enum EHandicap {NO_HANDICAP, MILD, SEVERE};
 	EHandicap handicap;//0-no, 1-mild, 2-severe
@@ -73,7 +73,7 @@ struct DLL_LINKAGE PlayerSettings
 		h & castle;
 		h & hero;
 		h & heroPortrait;
-		h & heroName;
+		h & heroNameTextId;
 		h & bonus;
 		h & color;
 		h & handicap;

+ 9 - 9
lib/campaign/CampaignHandler.cpp

@@ -134,7 +134,7 @@ std::string CampaignHandler::readLocalizedString(CBinaryReader & reader, std::st
 		return "";
 
 	VLC->generaltexth->registerString(modName, stringID, input);
-	return VLC->generaltexth->translate(stringID.get());
+	return stringID.get();
 }
 
 void CampaignHandler::readHeaderFromJson(CampaignHeader & ret, JsonNode & reader, std::string filename, std::string modName, std::string encoding)
@@ -149,8 +149,8 @@ void CampaignHandler::readHeaderFromJson(CampaignHeader & ret, JsonNode & reader
 	ret.version = CampaignVersion::VCMI;
 	ret.campaignRegions = CampaignRegions::fromJson(reader["regions"]);
 	ret.numberOfScenarios = reader["scenarios"].Vector().size();
-	ret.name = reader["name"].String();
-	ret.description = reader["description"].String();
+	ret.name.appendTextID(reader["name"].String());
+	ret.description.appendTextID(reader["description"].String());
 	ret.difficultyChoosenByPlayer = reader["allowDifficultySelection"].Bool();
 	ret.music = AudioPath::fromJson(reader["music"]);
 	ret.filename = filename;
@@ -169,7 +169,7 @@ CampaignScenario CampaignHandler::readScenarioFromJson(JsonNode & reader)
 			ret.prologVideo = VideoPath::fromJson(identifier["video"]);
 			ret.prologMusic = AudioPath::fromJson(identifier["music"]);
 			ret.prologVoice = AudioPath::fromJson(identifier["voice"]);
-			ret.prologText = identifier["text"].String();
+			ret.prologText.jsonDeserialize(identifier["text"]);
 		}
 		return ret;
 	};
@@ -181,7 +181,7 @@ CampaignScenario CampaignHandler::readScenarioFromJson(JsonNode & reader)
 
 	ret.regionColor = reader["color"].Integer();
 	ret.difficulty = reader["difficulty"].Integer();
-	ret.regionText = reader["regionText"].String();
+	ret.regionText.jsonDeserialize(reader["regionText"]);
 	ret.prolog = prologEpilogReader(reader["prolog"]);
 	ret.epilog = prologEpilogReader(reader["epilog"]);
 
@@ -383,8 +383,8 @@ void CampaignHandler::readHeaderFromMemory( CampaignHeader & ret, CBinaryReader
 	ret.version = static_cast<CampaignVersion>(reader.readUInt32());
 	ui8 campId = reader.readUInt8() - 1;//change range of it from [1, 20] to [0, 19]
 	ret.loadLegacyData(campId);
-	ret.name = readLocalizedString(reader, filename, modName, encoding, "name");
-	ret.description = readLocalizedString(reader, filename, modName, encoding, "description");
+	ret.name.appendTextID(readLocalizedString(reader, filename, modName, encoding, "name"));
+	ret.description.appendTextID(readLocalizedString(reader, filename, modName, encoding, "description"));
 	if (ret.version > CampaignVersion::RoE)
 		ret.difficultyChoosenByPlayer = reader.readInt8();
 	else
@@ -410,7 +410,7 @@ CampaignScenario CampaignHandler::readScenarioFromMemory( CBinaryReader & reader
 			ret.prologVideo = CampaignHandler::prologVideoName(index);
 			ret.prologMusic = CampaignHandler::prologMusicName(reader.readUInt8());
 			ret.prologVoice = isOriginalCampaign ? CampaignHandler::prologVoiceName(index) : AudioPath();
-			ret.prologText = readLocalizedString(reader, header.filename, header.modName, header.encoding, identifier);
+			ret.prologText.appendTextID(readLocalizedString(reader, header.filename, header.modName, header.encoding, identifier));
 		}
 		return ret;
 	};
@@ -428,7 +428,7 @@ CampaignScenario CampaignHandler::readScenarioFromMemory( CBinaryReader & reader
 	}
 	ret.regionColor = reader.readUInt8();
 	ret.difficulty = reader.readUInt8();
-	ret.regionText = readLocalizedString(reader, header.filename, header.modName, header.encoding, ret.mapName + ".region");
+	ret.regionText.appendTextID(readLocalizedString(reader, header.filename, header.modName, header.encoding, ret.mapName + ".region"));
 	ret.prolog = prologEpilogReader(ret.mapName + ".prolog");
 	ret.epilog = prologEpilogReader(ret.mapName + ".epilog");
 

+ 2 - 1
lib/campaign/CampaignScenarioPrologEpilog.h

@@ -10,6 +10,7 @@
 #pragma once
 
 #include "../filesystem/ResourcePath.h"
+#include "../MetaString.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
@@ -19,7 +20,7 @@ struct DLL_LINKAGE CampaignScenarioPrologEpilog
 	VideoPath prologVideo;
 	AudioPath prologMusic; // from CmpMusic.txt
 	AudioPath prologVoice;
-	std::string prologText;
+	MetaString prologText;
 
 	template <typename Handler> void serialize(Handler &h, const int formatVersion)
 	{

+ 5 - 5
lib/campaign/CampaignState.cpp

@@ -134,14 +134,14 @@ bool CampaignHeader::formatVCMI() const
 	return version == CampaignVersion::VCMI;
 }
 
-std::string CampaignHeader::getDescription() const
+std::string CampaignHeader::getDescriptionTranslated() const
 {
-	return description;
+	return description.toString();
 }
 
-std::string CampaignHeader::getName() const
+std::string CampaignHeader::getNameTranslated() const
 {
-	return name;
+	return name.toString();
 }
 
 std::string CampaignHeader::getFilename() const
@@ -267,7 +267,7 @@ void CampaignState::setCurrentMapAsConquered(std::vector<CGHeroInstance *> heroe
 		return a->getHeroStrength() > b->getHeroStrength();
 	});
 
-	logGlobal->info("Scenario %d of campaign %s (%s) has been completed", static_cast<int>(*currentMap), getFilename(), getName());
+	logGlobal->info("Scenario %d of campaign %s (%s) has been completed", static_cast<int>(*currentMap), getFilename(), getNameTranslated());
 
 	mapsConquered.push_back(*currentMap);
 	auto reservedHeroes = getReservedHeroes();

+ 7 - 6
lib/campaign/CampaignState.h

@@ -10,6 +10,7 @@
 #pragma once
 
 #include "../lib/GameConstants.h"
+#include "../lib/MetaString.h"
 #include "../lib/filesystem/ResourcePath.h"
 #include "CampaignConstants.h"
 #include "CampaignScenarioPrologEpilog.h"
@@ -74,8 +75,8 @@ class DLL_LINKAGE CampaignHeader : public boost::noncopyable
 
 	CampaignVersion version = CampaignVersion::NONE;
 	CampaignRegions campaignRegions;
-	std::string name;
-	std::string description;
+	MetaString name;
+	MetaString description;
 	AudioPath music;
 	std::string filename;
 	std::string modName;
@@ -90,8 +91,8 @@ public:
 	bool playerSelectedDifficulty() const;
 	bool formatVCMI() const;
 
-	std::string getDescription() const;
-	std::string getName() const;
+	std::string getDescriptionTranslated() const;
+	std::string getNameTranslated() const;
 	std::string getFilename() const;
 	std::string getModName() const;
 	std::string getEncoding() const;
@@ -176,12 +177,12 @@ struct DLL_LINKAGE CampaignTravel
 struct DLL_LINKAGE CampaignScenario
 {
 	std::string mapName; //*.h3m
-	std::string scenarioName; //from header. human-readble
+	MetaString scenarioName; //from header
 	std::set<CampaignScenarioID> preconditionRegions; //what we need to conquer to conquer this one (stored as bitfield in h3c)
 	ui8 regionColor = 0;
 	ui8 difficulty = 0;
 
-	std::string regionText;
+	MetaString regionText;
 	CampaignScenarioPrologEpilog prolog;
 	CampaignScenarioPrologEpilog epilog;
 

+ 1 - 1
lib/gameState/CGameState.cpp

@@ -1020,7 +1020,7 @@ void CGameState::initTowns()
 		if(vti->getNameTranslated().empty())
 		{
 			size_t nameID = getRandomGenerator().nextInt(vti->getTown()->getRandomNamesCount() - 1);
-			vti->setNameTranslated(vti->getTown()->getRandomNameTranslated(nameID));
+			vti->setNameTextId(vti->getTown()->getRandomNameTextID(nameID));
 		}
 
 		static const BuildingID basicDwellings[] = { BuildingID::DWELL_FIRST, BuildingID::DWELL_LVL_2, BuildingID::DWELL_LVL_3, BuildingID::DWELL_LVL_4, BuildingID::DWELL_LVL_5, BuildingID::DWELL_LVL_6, BuildingID::DWELL_LVL_7 };

+ 2 - 2
lib/mapObjects/CGCreature.cpp

@@ -108,7 +108,7 @@ void CGCreature::onHeroVisit( const CGHeroInstance * h ) const
 	{
 		InfoWindow iw;
 		iw.player = h->tempOwner;
-		iw.text.appendRawString(message);
+		iw.text = message;
 		iw.type = EInfoWindowMode::MODAL;
 		cb->showInfoDialog(&iw);
 	}
@@ -578,7 +578,7 @@ void CGCreature::serializeJsonOptions(JsonSerializeFormat & handler)
 
 	handler.serializeBool("noGrowing", notGrowingTeam);
 	handler.serializeBool("neverFlees", neverFlees);
-	handler.serializeString("rewardMessage", message);
+	handler.serializeStruct("rewardMessage", message);
 }
 
 VCMI_LIB_NAMESPACE_END

+ 2 - 1
lib/mapObjects/CGCreature.h

@@ -11,6 +11,7 @@
 
 #include "CArmedInstance.h"
 #include "../ResourceSet.h"
+#include "../MetaString.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
@@ -27,7 +28,7 @@ public:
 
 	ui32 identifier; //unique code for this monster (used in missions)
 	si8 character; //character of this set of creatures (0 - the most friendly, 4 - the most hostile) => on init changed to -4 (compliant) ... 10 value (savage)
-	std::string message; //message printed for attacking hero
+	MetaString message; //message printed for attacking hero
 	TResources resources; // resources given to hero that has won with monsters
 	ArtifactID gainedArtifact; //ID of artifact gained to hero, -1 if none
 	bool neverFlees; //if true, the troops will never flee

+ 6 - 11
lib/mapObjects/CGHeroInstance.cpp

@@ -1030,15 +1030,13 @@ si32 CGHeroInstance::manaLimit() const
 
 std::string CGHeroInstance::getNameTranslated() const
 {
-	if (!nameCustom.empty())
-		return nameCustom;
 	return VLC->generaltexth->translate(getNameTextID());
 }
 
 std::string CGHeroInstance::getNameTextID() const
 {
-	if (!nameCustom.empty())
-		return nameCustom;
+	if (!nameCustomTextId.empty())
+		return nameCustomTextId;
 	if (type)
 		return type->getNameTextID();
 
@@ -1049,16 +1047,13 @@ std::string CGHeroInstance::getNameTextID() const
 
 std::string CGHeroInstance::getBiographyTranslated() const
 {
-	if (!biographyCustom.empty())
-		return biographyCustom;
-
 	return VLC->generaltexth->translate(getBiographyTextID());
 }
 
 std::string CGHeroInstance::getBiographyTextID() const
 {
-	if (!biographyCustom.empty())
-		return biographyCustom;
+	if (!biographyCustomTextId.empty())
+		return biographyCustomTextId;
 	if (type)
 		return type->getBiographyTextID();
 
@@ -1520,7 +1515,7 @@ void CGHeroInstance::updateFrom(const JsonNode & data)
 
 void CGHeroInstance::serializeCommonOptions(JsonSerializeFormat & handler)
 {
-	handler.serializeString("biography", biographyCustom);
+	handler.serializeString("biography", biographyCustomTextId);
 	handler.serializeInt("experience", exp, 0);
 
 	if(!handler.saving && exp != UNINITIALIZED_EXPERIENCE) //do not gain levels if experience is not initialized
@@ -1531,7 +1526,7 @@ void CGHeroInstance::serializeCommonOptions(JsonSerializeFormat & handler)
 		}
 	}
 
-	handler.serializeString("name", nameCustom);
+	handler.serializeString("name", nameCustomTextId);
 	handler.serializeInt("gender", gender, 0);
 
 	{

+ 4 - 4
lib/mapObjects/CGHeroInstance.h

@@ -74,8 +74,8 @@ public:
 	std::vector<std::pair<SecondarySkill,ui8> > secSkills; //first - ID of skill, second - level of skill (1 - basic, 2 - adv., 3 - expert); if hero has ability (-1, -1) it meansthat it should have default secondary abilities
 	EHeroGender gender;
 
-	std::string nameCustom;
-	std::string biographyCustom;
+	std::string nameCustomTextId;
+	std::string biographyCustomTextId;
 
 	bool inTownGarrison; // if hero is in town garrison
 	ConstTransitivePtr<CGTownInstance> visitedTown; //set if hero is visiting town or in the town garrison
@@ -319,8 +319,8 @@ public:
 		h & static_cast<CArtifactSet&>(*this);
 		h & exp;
 		h & level;
-		h & nameCustom;
-		h & biographyCustom;
+		h & nameCustomTextId;
+		h & biographyCustomTextId;
 		h & portrait;
 		h & mana;
 		h & secSkills;

+ 4 - 4
lib/mapObjects/CGPandoraBox.cpp

@@ -35,7 +35,7 @@ void CGPandoraBox::init()
 	{
 		i.reward.removeObject = true;
 		if(!message.empty() && i.message.empty())
-			i.message = MetaString::createFromRawString(message);
+			i.message = message;
 	}
 }
 
@@ -209,7 +209,7 @@ void CGPandoraBox::serializeJsonOptions(JsonSerializeFormat & handler)
 {
 	CRewardableObject::serializeJsonOptions(handler);
 	
-	handler.serializeString("guardMessage", message);
+	handler.serializeStruct("guardMessage", message);
 	
 	if(!handler.saving)
 	{
@@ -297,7 +297,7 @@ void CGEvent::init()
 	{
 		i.reward.removeObject = removeAfterVisit;
 		if(!message.empty() && i.message.empty())
-			i.message = MetaString::createFromRawString(message);
+			i.message = message;
 	}
 }
 
@@ -327,7 +327,7 @@ void CGEvent::activated( const CGHeroInstance * h ) const
 		InfoWindow iw;
 		iw.player = h->tempOwner;
 		if(!message.empty())
-			iw.text.appendRawString(message);
+			iw.text = message;
 		else
 			iw.text.appendLocalString(EMetaText::ADVOB_TXT, 16);
 		cb->showInfoDialog(&iw);

+ 1 - 1
lib/mapObjects/CGPandoraBox.h

@@ -19,7 +19,7 @@ struct InfoWindow;
 class DLL_LINKAGE CGPandoraBox : public CRewardableObject
 {
 public:
-	std::string message;
+	MetaString message;
 
 	void initObj(CRandomGenerator & rand) override;
 	void onHeroVisit(const CGHeroInstance * h) const override;

+ 10 - 10
lib/mapObjects/CGTownInstance.cpp

@@ -327,7 +327,7 @@ void CGTownInstance::onHeroVisit(const CGHeroInstance * h) const
 	}
 	else
 	{
-		logGlobal->error("%s visits allied town of %s from different pos?", h->getNameTranslated(), name);
+		logGlobal->error("%s visits allied town of %s from different pos?", h->getNameTranslated(), getNameTranslated());
 	}
 }
 
@@ -337,15 +337,15 @@ void CGTownInstance::onHeroLeave(const CGHeroInstance * h) const
 	if(visitingHero == h)
 	{
 		cb->stopHeroVisitCastle(this, h);
-		logGlobal->trace("%s correctly left town %s", h->getNameTranslated(), name);
+		logGlobal->trace("%s correctly left town %s", h->getNameTranslated(), getNameTranslated());
 	}
 	else
-		logGlobal->warn("Warning, %s tries to leave the town %s but hero is not inside.", h->getNameTranslated(), name);
+		logGlobal->warn("Warning, %s tries to leave the town %s but hero is not inside.", h->getNameTranslated(), getNameTranslated());
 }
 
 std::string CGTownInstance::getObjectName() const
 {
-	return name + ", " + town->faction->getNameTranslated();
+	return getNameTranslated() + ", " + town->faction->getNameTranslated();
 }
 
 bool CGTownInstance::townEnvisagesBuilding(BuildingSubID::EBuildingSubID subId) const
@@ -767,7 +767,7 @@ void CGTownInstance::updateAppearance()
 
 std::string CGTownInstance::nodeName() const
 {
-	return "Town (" + (town ? town->faction->getNameTranslated() : "unknown") + ") of " +  name;
+	return "Town (" + (town ? town->faction->getNameTranslated() : "unknown") + ") of " + getNameTranslated();
 }
 
 void CGTownInstance::deserializationFix()
@@ -915,12 +915,12 @@ CBonusSystemNode & CGTownInstance::whatShouldBeAttached()
 
 std::string CGTownInstance::getNameTranslated() const
 {
-	return name;
+	return VLC->generaltexth->translate(nameTextId);
 }
 
-void CGTownInstance::setNameTranslated( const std::string & newName )
+void CGTownInstance::setNameTextId( const std::string & newName )
 {
-	name = newName;
+	nameTextId = newName;
 }
 
 const CArmedInstance * CGTownInstance::getUpperArmy() const
@@ -980,7 +980,7 @@ TResources CGTownInstance::getBuildingCost(const BuildingID & buildingID) const
 		return town->buildings.at(buildingID)->resources;
 	else
 	{
-		logGlobal->error("Town %s at %s has no possible building %d!", name, pos.toString(), buildingID.toEnum());
+		logGlobal->error("Town %s at %s has no possible building %d!", getNameTranslated(), pos.toString(), buildingID.toEnum());
 		return TResources();
 	}
 
@@ -1097,7 +1097,7 @@ void CGTownInstance::serializeJsonOptions(JsonSerializeFormat & handler)
 	if(!handler.saving)
 		handler.serializeEnum("tightFormation", formation, NArmyFormation::names); //for old format
 	CArmedInstance::serializeJsonOptions(handler);
-	handler.serializeString("name", name);
+	handler.serializeString("name", nameTextId);
 
 	{
 		auto decodeBuilding = [this](const std::string & identifier) -> si32

+ 4 - 4
lib/mapObjects/CGTownInstance.h

@@ -44,7 +44,7 @@ struct DLL_LINKAGE GrowthInfo
 
 class DLL_LINKAGE CGTownInstance : public CGDwelling, public IShipyard, public IMarket, public INativeTerrainProvider, public ICreatureUpgrader
 {
-	std::string name; // name of town
+	std::string nameTextId; // name of town
 public:
 	using CGDwelling::getPosition;
 
@@ -73,7 +73,7 @@ public:
 	template <typename Handler> void serialize(Handler &h, const int version)
 	{
 		h & static_cast<CGDwelling&>(*this);
-		h & name;
+		h & nameTextId;
 		h & builded;
 		h & destroyed;
 		h & identifier;
@@ -102,7 +102,7 @@ public:
 			{
 				if(!town->buildings.count(building) || !town->buildings.at(building))
 				{
-					logGlobal->error("#1444-like issue in CGTownInstance::serialize. From town %s at %s removing the bogus builtBuildings item %s", name, pos.toString(), building);
+					logGlobal->error("#1444-like issue in CGTownInstance::serialize. From town %s at %s removing the bogus builtBuildings item %s", nameTextId, pos.toString(), building);
 					return true;
 				}
 				return false;
@@ -126,7 +126,7 @@ public:
 	const CArmedInstance *getUpperArmy() const; //garrisoned hero if present or the town itself
 
 	std::string getNameTranslated() const;
-	void setNameTranslated(const std::string & newName);
+	void setNameTextId(const std::string & newName);
 
 	//////////////////////////////////////////////////////////////////////////
 

+ 33 - 30
lib/mapObjects/CQuest.cpp

@@ -181,20 +181,20 @@ bool CQuest::checkQuest(const CGHeroInstance * h) const
 
 void CQuest::getVisitText(MetaString &iwText, std::vector<Component> &components, bool isCustom, bool firstVisit, const CGHeroInstance * h) const
 {
-	std::string text;
+	MetaString text;
 	bool failRequirements = (h ? !checkQuest(h) : true);
 
 	if(firstVisit)
 	{
 		isCustom = isCustomFirst;
 		text = firstVisitText;
-		iwText.appendRawString(text);
+		iwText.appendRawString(text.toString());
 	}
 	else if(failRequirements)
 	{
 		isCustom = isCustomNext;
 		text = nextVisitText;
-		iwText.appendRawString(text);
+		iwText.appendRawString(text.toString());
 	}
 	switch (missionType)
 	{
@@ -223,7 +223,7 @@ void CQuest::getVisitText(MetaString &iwText, std::vector<Component> &components
 		case MISSION_KILL_HERO:
 			components.emplace_back(Component::EComponentType::HERO_PORTRAIT, heroPortrait, 0, 0);
 			if(!isCustom)
-				addReplacements(iwText, text);
+				addReplacements(iwText, text.toString());
 			break;
 		case MISSION_HERO:
 			//FIXME: portrait may not match hero, if custom portrait was set in map editor
@@ -236,7 +236,7 @@ void CQuest::getVisitText(MetaString &iwText, std::vector<Component> &components
 				components.emplace_back(stackToKill);
 				if(!isCustom)
 				{
-					addReplacements(iwText, text);
+					addReplacements(iwText, text.toString());
 				}
 			}
 			break;
@@ -286,7 +286,7 @@ void CQuest::getVisitText(MetaString &iwText, std::vector<Component> &components
 		case MISSION_PLAYER:
 			components.emplace_back(Component::EComponentType::FLAG, m13489val, 0, 0);
 			if(!isCustom)
-				iwText.replaceRawString(VLC->generaltexth->colors[m13489val]);
+				iwText.replaceLocalString(EMetaText::COLOR, m13489val);
 			break;
 	}
 }
@@ -380,7 +380,7 @@ void CQuest::getRolloverText(MetaString &ms, bool onHover) const
 
 void CQuest::getCompletionText(MetaString &iwText) const
 {
-	iwText.appendRawString(completedText);
+	iwText.appendRawString(completedText.toString());
 	switch(missionType)
 	{
 		case CQuest::MISSION_LEVEL:
@@ -388,22 +388,22 @@ void CQuest::getCompletionText(MetaString &iwText) const
 				iwText.replaceNumber(m13489val);
 			break;
 		case CQuest::MISSION_PRIMARY_STAT:
-			if (vstd::contains (completedText,'%')) //there's one case when there's nothing to replace
+		{
+			MetaString loot;
+			assert(m2stats.size() <= 4);
+			for (int i = 0; i < m2stats.size(); ++i)
 			{
-				MetaString loot;
-				for (int i = 0; i < 4; ++i)
+				if (m2stats[i])
 				{
-					if (m2stats[i])
-					{
-						loot.appendRawString("%d %s");
-						loot.replaceNumber(m2stats[i]);
-						loot.replaceRawString(VLC->generaltexth->primarySkillNames[i]);
-					}
+					loot.appendRawString("%d %s");
+					loot.replaceNumber(m2stats[i]);
+					loot.replaceRawString(VLC->generaltexth->primarySkillNames[i]);
 				}
-				if (!isCustomComplete)
-					iwText.replaceRawString(loot.buildList());
 			}
+			if (!isCustomComplete)
+				iwText.replaceRawString(loot.buildList());
 			break;
+		}
 		case CQuest::MISSION_ART:
 		{
 			MetaString loot;
@@ -447,7 +447,7 @@ void CQuest::getCompletionText(MetaString &iwText) const
 		case MISSION_KILL_HERO:
 		case MISSION_KILL_CREATURE:
 			if (!isCustomComplete)
-				addReplacements(iwText, completedText);
+				addReplacements(iwText, completedText.toString());
 			break;
 		case MISSION_HERO:
 			if (!isCustomComplete)
@@ -470,9 +470,9 @@ void CQuest::serializeJson(JsonSerializeFormat & handler, const std::string & fi
 {
 	auto q = handler.enterStruct(fieldName);
 
-	handler.serializeString("firstVisitText", firstVisitText);
-	handler.serializeString("nextVisitText", nextVisitText);
-	handler.serializeString("completedText", completedText);
+	handler.serializeStruct("firstVisitText", firstVisitText);
+	handler.serializeStruct("nextVisitText", nextVisitText);
+	handler.serializeStruct("completedText", completedText);
 
 	if(!handler.saving)
 	{
@@ -589,16 +589,16 @@ void CGSeerHut::initObj(CRandomGenerator & rand)
 		std::string questName  = quest->missionName(quest->missionType);
 
 		if(!quest->isCustomFirst)
-			quest->firstVisitText = VLC->generaltexth->translate("core.seerhut.quest." + questName + "." + quest->missionState(0), quest->textOption);
+			quest->firstVisitText.appendTextID(TextIdentifier("core", "seerhut", "quest", questName, quest->missionState(0), quest->textOption).get());
 		if(!quest->isCustomNext)
-			quest->nextVisitText = VLC->generaltexth->translate("core.seerhut.quest." + questName + "." + quest->missionState(1), quest->textOption);
+			quest->firstVisitText.appendTextID(TextIdentifier("core", "seerhut", "quest", questName, quest->missionState(1), quest->textOption).get());
 		if(!quest->isCustomComplete)
-			quest->completedText = VLC->generaltexth->translate("core.seerhut.quest." + questName + "." + quest->missionState(2), quest->textOption);
+			quest->firstVisitText.appendTextID(TextIdentifier("core", "seerhut", "quest", questName, quest->missionState(2), quest->textOption).get());
 	}
 	else
 	{
 		quest->progress = CQuest::COMPLETE;
-		quest->firstVisitText = VLC->generaltexth->seerEmpty[quest->completedOption];
+		quest->firstVisitText.appendTextID(TextIdentifier("core", "seehut", "empty", quest->completedOption).get());
 	}
 }
 
@@ -632,14 +632,17 @@ void CQuest::addReplacements(MetaString &out, const std::string &base) const
 	switch(missionType)
 	{
 	case MISSION_KILL_CREATURE:
-		out.replaceCreatureName(stackToKill);
-		if (std::count(base.begin(), base.end(), '%') == 2) //say where is placed monster
+		if(stackToKill.type)
 		{
-			out.replaceRawString(VLC->generaltexth->arraytxt[147+stackDirection]);
+			out.replaceCreatureName(stackToKill);
+			if (std::count(base.begin(), base.end(), '%') == 2) //say where is placed monster
+			{
+				out.replaceRawString(VLC->generaltexth->arraytxt[147+stackDirection]);
+			}
 		}
 		break;
 	case MISSION_KILL_HERO:
-		out.replaceRawString(heroName);
+		out.replaceTextID(heroName);
 		break;
 	}
 }

+ 2 - 1
lib/mapObjects/CQuest.h

@@ -11,6 +11,7 @@
 
 #include "CRewardableObject.h"
 #include "../ResourceSet.h"
+#include "../MetaString.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
@@ -70,7 +71,7 @@ public:
 	std::string heroName; //backup of hero name
 	si32 heroPortrait;
 
-	std::string firstVisitText, nextVisitText, completedText;
+	MetaString firstVisitText, nextVisitText, completedText;
 	bool isCustomFirst;
 	bool isCustomNext;
 	bool isCustomComplete;

+ 15 - 15
lib/mapObjects/MiscObjects.cpp

@@ -268,7 +268,7 @@ void CGResource::onHeroVisit( const CGHeroInstance * h ) const
 		{
 			BlockingDialog ynd(true,false);
 			ynd.player = h->getOwner();
-			ynd.text.appendRawString(message);
+			ynd.text = message;
 			cb->showBlockingDialog(&ynd);
 		}
 		else
@@ -288,7 +288,7 @@ void CGResource::collectRes(const PlayerColor & player) const
 	if(!message.empty())
 	{
 		sii.type = EInfoWindowMode::AUTO;
-		sii.text.appendRawString(message);
+		sii.text = message;
 	}
 	else
 	{
@@ -320,7 +320,7 @@ void CGResource::serializeJsonOptions(JsonSerializeFormat & handler)
 	if(!handler.saving && !handler.getCurrent()["guards"].Vector().empty())
 		CCreatureSet::serializeJson(handler, "guards", 7);
 	handler.serializeInt("amount", amount, 0);
-	handler.serializeString("guardMessage", message);
+	handler.serializeStruct("guardMessage", message);
 }
 
 bool CGTeleport::isEntrance() const
@@ -728,8 +728,8 @@ void CGArtifact::onHeroVisit(const CGHeroInstance * h) const
 			case Obj::ARTIFACT:
 			{
 				iw.components.emplace_back(Component::EComponentType::ARTIFACT, subID, 0, 0);
-				if(message.length())
-					iw.text.appendRawString(message);
+				if(!message.empty())
+					iw.text = message;
 				else
 					iw.text.appendLocalString(EMetaText::ART_EVNTS, subID);
 			}
@@ -738,8 +738,8 @@ void CGArtifact::onHeroVisit(const CGHeroInstance * h) const
 			{
 				int spellID = storedArtifact->getScrollSpellID();
 				iw.components.emplace_back(Component::EComponentType::SPELL, spellID, 0, 0);
-				if(message.length())
-					iw.text.appendRawString(message);
+				if(!message.empty())
+					iw.text = message;
 				else
 				{
 					iw.text.appendLocalString(EMetaText::ADVOB_TXT,135);
@@ -764,8 +764,8 @@ void CGArtifact::onHeroVisit(const CGHeroInstance * h) const
 			{
 				BlockingDialog ynd(true,false);
 				ynd.player = h->getOwner();
-				if(message.length())
-					ynd.text.appendRawString(message);
+				if(!message.empty())
+					ynd.text = message;
 				else
 				{
 					// TODO: Guard text is more complex in H3, see mantis issue 2325 for details
@@ -779,11 +779,11 @@ void CGArtifact::onHeroVisit(const CGHeroInstance * h) const
 			break;
 		case Obj::SPELL_SCROLL:
 			{
-				if(message.length())
+				if(!message.empty())
 				{
 					BlockingDialog ynd(true,false);
 					ynd.player = h->getOwner();
-					ynd.text.appendRawString(message);
+					ynd.text = message;
 					cb->showBlockingDialog(&ynd);
 				}
 				else
@@ -828,7 +828,7 @@ void CGArtifact::afterAddToMap(CMap * map)
 
 void CGArtifact::serializeJsonOptions(JsonSerializeFormat& handler)
 {
-	handler.serializeString("guardMessage", message);
+	handler.serializeStruct("guardMessage", message);
 	CArmedInstance::serializeJsonOptions(handler);
 	if(!handler.saving && !handler.getCurrent()["guards"].Vector().empty())
 		CCreatureSet::serializeJson(handler, "guards", 7);
@@ -1046,7 +1046,7 @@ void CGSignBottle::initObj(CRandomGenerator & rand)
 	{
 		auto vector = VLC->generaltexth->findStringsWithPrefix("core.randsign");
 		std::string messageIdentifier = *RandomGeneratorUtil::nextItem(vector, rand);
-		message = VLC->generaltexth->translate(messageIdentifier);
+		message.appendTextID(TextIdentifier("core", "randsign", messageIdentifier).get());
 	}
 
 	if(ID == Obj::OCEAN_BOTTLE)
@@ -1059,7 +1059,7 @@ void CGSignBottle::onHeroVisit( const CGHeroInstance * h ) const
 {
 	InfoWindow iw;
 	iw.player = h->getOwner();
-	iw.text.appendRawString(message);
+	iw.text = message;
 	cb->showInfoDialog(&iw);
 
 	if(ID == Obj::OCEAN_BOTTLE)
@@ -1068,7 +1068,7 @@ void CGSignBottle::onHeroVisit( const CGHeroInstance * h ) const
 
 void CGSignBottle::serializeJsonOptions(JsonSerializeFormat& handler)
 {
-	handler.serializeString("text", message);
+	handler.serializeStruct("text", message);
 }
 
 void CGScholar::onHeroVisit( const CGHeroInstance * h ) const

+ 3 - 3
lib/mapObjects/MiscObjects.h

@@ -43,7 +43,7 @@ public:
 class DLL_LINKAGE CGSignBottle : public CGObjectInstance //signs and ocean bottles
 {
 public:
-	std::string message;
+	MetaString message;
 
 	void onHeroVisit(const CGHeroInstance * h) const override;
 	void initObj(CRandomGenerator & rand) override;
@@ -119,7 +119,7 @@ class DLL_LINKAGE CGArtifact : public CArmedInstance
 {
 public:
 	CArtifactInstance * storedArtifact = nullptr;
-	std::string message;
+	MetaString message;
 
 	void onHeroVisit(const CGHeroInstance * h) const override;
 	void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override;
@@ -149,7 +149,7 @@ public:
 	static constexpr ui32 RANDOM_AMOUNT = 0;
 	ui32 amount = RANDOM_AMOUNT; //0 if random
 	
-	std::string message;
+	MetaString message;
 
 	void onHeroVisit(const CGHeroInstance * h) const override;
 	void initObj(CRandomGenerator & rand) override;

+ 2 - 2
lib/mapping/CMap.cpp

@@ -32,7 +32,7 @@ VCMI_LIB_NAMESPACE_BEGIN
 void Rumor::serializeJson(JsonSerializeFormat & handler)
 {
 	handler.serializeString("name", name);
-	handler.serializeString("text", text);
+	handler.serializeStruct("text", text);
 }
 
 DisposedHero::DisposedHero() : heroId(0), portrait(255)
@@ -59,7 +59,7 @@ bool CMapEvent::earlierThanOrEqual(const CMapEvent & other) const
 void CMapEvent::serializeJson(JsonSerializeFormat & handler)
 {
 	handler.serializeString("name", name);
-	handler.serializeString("message", message);
+	handler.serializeStruct("message", message);
 	handler.serializeInt("players", players);
 	handler.serializeInt("humanAffected", humanAffected);
 	handler.serializeInt("computerAffected", computerAffected);

+ 2 - 1
lib/mapping/CMap.h

@@ -11,6 +11,7 @@
 #pragma once
 
 #include "CMapHeader.h"
+#include "../MetaString.h"
 #include "../mapObjects/MiscObjects.h" // To serialize static props
 #include "../mapObjects/CQuest.h" // To serialize static props
 #include "../mapObjects/CGTownInstance.h" // To serialize static props
@@ -36,7 +37,7 @@ struct TeleportChannel;
 struct DLL_LINKAGE Rumor
 {
 	std::string name;
-	std::string text;
+	MetaString text;
 
 	Rumor() = default;
 	~Rumor() = default;

+ 4 - 3
lib/mapping/CMapDefines.h

@@ -10,9 +10,10 @@
 
 #pragma once
 
-VCMI_LIB_NAMESPACE_BEGIN
-
 #include "../ResourceSet.h"
+#include "../MetaString.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
 
 class TerrainType;
 class RiverType;
@@ -33,7 +34,7 @@ public:
 	bool earlierThanOrEqual(const CMapEvent & other) const;
 
 	std::string name;
-	std::string message;
+	MetaString message;
 	TResources resources;
 	ui8 players; // affected players, bit field?
 	ui8 humanAffected;

+ 79 - 1
lib/mapping/CMapHeader.cpp

@@ -15,7 +15,9 @@
 #include "../VCMI_Lib.h"
 #include "../CTownHandler.h"
 #include "../CGeneralTextHandler.h"
+#include "../modding/CModHandler.h"
 #include "../CHeroHandler.h"
+#include "../Languages.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
@@ -62,7 +64,7 @@ bool PlayerInfo::canAnyonePlay() const
 
 bool PlayerInfo::hasCustomMainHero() const
 {
-	return !mainCustomHeroName.empty() && mainCustomHeroPortrait != -1;
+	return !mainCustomHeroNameTextId.empty() && mainCustomHeroPortrait != -1;
 }
 
 EventCondition::EventCondition(EWinLoseType condition):
@@ -127,6 +129,12 @@ CMapHeader::CMapHeader() : version(EMapFormat::VCMI), height(72), width(72),
 	setupEvents();
 	allowedHeroes = VLC->heroh->getDefaultAllowed();
 	players.resize(PlayerColor::PLAYER_LIMIT_I);
+	VLC->generaltexth->addSubContainer(*this);
+}
+
+CMapHeader::~CMapHeader()
+{
+	VLC->generaltexth->removeSubContainer(*this);
 }
 
 ui8 CMapHeader::levels() const
@@ -134,4 +142,74 @@ ui8 CMapHeader::levels() const
 	return (twoLevel ? 2 : 1);
 }
 
+void CMapHeader::registerMapStrings()
+{
+	//get supported languages. Assuming that translation containing most strings is the base language
+	std::set<std::string> mapLanguages, mapBaseLanguages;
+	int maxStrings = 0;
+	for(auto & translation : translations.Struct())
+	{
+		if(translation.first.empty() || !translation.second.isStruct() || translation.second.Struct().empty())
+			continue;
+		
+		if(translation.second.Struct().size() > maxStrings)
+			maxStrings = translation.second.Struct().size();
+		mapLanguages.insert(translation.first);
+	}
+	
+	if(maxStrings == 0 || mapBaseLanguages.empty())
+	{
+		logGlobal->info("Map %s doesn't have any supported translation", name.toString());
+		return;
+	}
+	
+	//identifying base languages
+	for(auto & translation : translations.Struct())
+	{
+		if(translation.second.isStruct() && translation.second.Struct().size() == maxStrings)
+			mapBaseLanguages.insert(translation.first);
+	}
+	
+	std::string baseLanguage, language;
+	//english is preferrable as base language
+	if(mapBaseLanguages.count(Languages::getLanguageOptions(Languages::ELanguages::ENGLISH).identifier))
+		baseLanguage = Languages::getLanguageOptions(Languages::ELanguages::ENGLISH).identifier;
+	else
+		baseLanguage = *mapBaseLanguages.begin();
+
+	if(mapBaseLanguages.count(CGeneralTextHandler::getPreferredLanguage()))
+	{
+		language = CGeneralTextHandler::getPreferredLanguage(); //preferred language is base language - use it
+		baseLanguage = language;
+	}
+	else
+	{
+		if(mapLanguages.count(CGeneralTextHandler::getPreferredLanguage()))
+			language = CGeneralTextHandler::getPreferredLanguage();
+		else
+			language = baseLanguage; //preferred language is not supported, use base language
+	}
+	
+	assert(!language.empty());
+	
+	JsonNode data = translations[baseLanguage];
+	if(language != baseLanguage)
+		JsonUtils::mergeCopy(data, translations[language]);
+	
+	for(auto & s : data.Struct())
+		registerString("map", TextIdentifier(s.first), s.second.String(), language);
+}
+
+std::string mapRegisterLocalizedString(const std::string & modContext, CMapHeader & mapHeader, const TextIdentifier & UID, const std::string & localized)
+{
+	return mapRegisterLocalizedString(modContext, mapHeader, UID, localized, VLC->modh->getModLanguage(modContext));
+}
+
+std::string mapRegisterLocalizedString(const std::string & modContext, CMapHeader & mapHeader, const TextIdentifier & UID, const std::string & localized, const std::string & language)
+{
+	mapHeader.registerString(modContext, UID, localized, language);
+	mapHeader.translations.Struct()[language].Struct()[UID.get()].String() = localized;
+	return UID.get();
+}
+
 VCMI_LIB_NAMESPACE_END

+ 20 - 6
lib/mapping/CMapHeader.h

@@ -14,6 +14,7 @@
 #include "../LogicalExpression.h"
 #include "../int3.h"
 #include "../MetaString.h"
+#include "../CGeneralTextHandler.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
@@ -63,7 +64,7 @@ struct DLL_LINKAGE PlayerInfo
 	bool hasRandomHero;
 	/// The default value is -1.
 	si32 mainCustomHeroPortrait;
-	std::string mainCustomHeroName;
+	std::string mainCustomHeroNameTextId;
 	/// ID of custom hero (only if portrait and hero name are set, otherwise unpredicted value), -1 if none (not always -1)
 	si32 mainCustomHeroId;
 
@@ -84,7 +85,7 @@ struct DLL_LINKAGE PlayerInfo
 		h & allowedFactions;
 		h & isFactionRandom;
 		h & mainCustomHeroPortrait;
-		h & mainCustomHeroName;
+		h & mainCustomHeroNameTextId;
 		h & heroesNames;
 		h & hasMainTown;
 		h & generateHeroAtMainTown;
@@ -199,7 +200,7 @@ struct DLL_LINKAGE TriggeredEvent
 };
 
 /// The map header holds information about loss/victory condition,map format, version, players, height, width,...
-class DLL_LINKAGE CMapHeader
+class DLL_LINKAGE CMapHeader: public TextLocalizationContainer
 {
 	void setupEvents();
 public:
@@ -213,7 +214,7 @@ public:
 	static const int MAP_SIZE_GIANT = 252;
 
 	CMapHeader();
-	virtual ~CMapHeader() = default;
+	virtual ~CMapHeader();
 
 	ui8 levels() const;
 
@@ -223,8 +224,8 @@ public:
 	si32 height; /// The default value is 72.
 	si32 width; /// The default value is 72.
 	bool twoLevel; /// The default value is true.
-	std::string name;
-	std::string description;
+	MetaString name;
+	MetaString description;
 	ui8 difficulty; /// The default value is 1 representing a normal map difficulty.
 	/// Specifies the maximum level to reach for a hero. A value of 0 states that there is no
 	///	maximum level for heroes. This is the default value.
@@ -244,10 +245,16 @@ public:
 
 	/// "main quests" of the map that describe victory and loss conditions
 	std::vector<TriggeredEvent> triggeredEvents;
+	
+	/// translations for map to be transferred over network
+	JsonNode translations;
+	
+	void registerMapStrings();
 
 	template <typename Handler>
 	void serialize(Handler & h, const int Version)
 	{
+		h & static_cast<TextLocalizationContainer&>(*this);
 		h & version;
 		h & mods;
 		h & name;
@@ -267,7 +274,14 @@ public:
 		h & victoryIconIndex;
 		h & defeatMessage;
 		h & defeatIconIndex;
+		h & translations;
+		if(!h.saving)
+			registerMapStrings();
 	}
 };
 
+/// wrapper functions to register string into the map and stores its translation
+std::string DLL_LINKAGE mapRegisterLocalizedString(const std::string & modContext, CMapHeader & mapHeader, const TextIdentifier & UID, const std::string & localized);
+std::string DLL_LINKAGE mapRegisterLocalizedString(const std::string & modContext, CMapHeader & mapHeader, const TextIdentifier & UID, const std::string & localized, const std::string & language);
+
 VCMI_LIB_NAMESPACE_END

+ 9 - 9
lib/mapping/CMapInfo.cpp

@@ -100,12 +100,12 @@ void CMapInfo::countPlayers()
 				amountOfHumanPlayersInSave++;
 }
 
-std::string CMapInfo::getName() const
+std::string CMapInfo::getNameTranslated() const
 {
-	if(campaign && !campaign->getName().empty())
-		return campaign->getName();
-	else if(mapHeader && mapHeader->name.length())
-		return mapHeader->name;
+	if(campaign && !campaign->getNameTranslated().empty())
+		return campaign->getNameTranslated();
+	else if(mapHeader && !mapHeader->name.empty())
+		return mapHeader->name.toString();
 	else
 		return VLC->generaltexth->allTexts[508];
 }
@@ -121,16 +121,16 @@ std::string CMapInfo::getNameForList() const
 	}
 	else
 	{
-		return getName();
+		return getNameTranslated();
 	}
 }
 
-std::string CMapInfo::getDescription() const
+std::string CMapInfo::getDescriptionTranslated() const
 {
 	if(campaign)
-		return campaign->getDescription();
+		return campaign->getDescriptionTranslated();
 	else
-		return mapHeader->description;
+		return mapHeader->description.toString();
 }
 
 int CMapInfo::getMapSizeIconId() const

+ 3 - 3
lib/mapping/CMapInfo.h

@@ -49,10 +49,10 @@ public:
 	void saveInit(const ResourcePath & file);
 	void campaignInit();
 	void countPlayers();
-	// TODO: Those must be on client-side
-	std::string getName() const;
+	
+	std::string getNameTranslated() const;
 	std::string getNameForList() const;
-	std::string getDescription() const;
+	std::string getDescriptionTranslated() const;
 	int getMapSizeIconId() const;
 	int getMapSizeFormatIconId() const;
 	std::string getMapSizeName() const;

+ 19 - 20
lib/mapping/MapFormatH3M.cpp

@@ -180,8 +180,8 @@ void CMapLoaderH3M::readHeader()
 	mapHeader->areAnyPlayers = reader->readBool();
 	mapHeader->height = mapHeader->width = reader->readInt32();
 	mapHeader->twoLevel = reader->readBool();
-	mapHeader->name = readLocalizedString("header.name");
-	mapHeader->description = readLocalizedString("header.description");
+	mapHeader->name.appendTextID(readLocalizedString("header.name"));
+	mapHeader->description.appendTextID(readLocalizedString("header.description"));
 	mapHeader->difficulty = reader->readInt8();
 
 	if(features.levelAB)
@@ -253,7 +253,7 @@ void CMapLoaderH3M::readPlayerInfo()
 		if(playerInfo.mainCustomHeroId != -1)
 		{
 			playerInfo.mainCustomHeroPortrait = reader->readHeroPortrait();
-			playerInfo.mainCustomHeroName = readLocalizedString(TextIdentifier("header", "player", i, "mainHeroName"));
+			playerInfo.mainCustomHeroNameTextId = readLocalizedString(TextIdentifier("header", "player", i, "mainHeroName"));
 		}
 
 		if(features.levelAB)
@@ -807,7 +807,7 @@ void CMapLoaderH3M::readRumors()
 	{
 		Rumor ourRumor;
 		ourRumor.name = readBasicString();
-		ourRumor.text = readLocalizedString(TextIdentifier("header", "rumor", it, "text"));
+		ourRumor.text.appendTextID(readLocalizedString(TextIdentifier("header", "rumor", it, "text")));
 		map->rumors.push_back(ourRumor);
 	}
 }
@@ -860,7 +860,7 @@ void CMapLoaderH3M::readPredefinedHeroes()
 
 		bool hasCustomBio = reader->readBool();
 		if(hasCustomBio)
-			hero->biographyCustom = readLocalizedString(TextIdentifier("heroes", heroID, "biography"));
+			hero->biographyCustomTextId = readLocalizedString(TextIdentifier("heroes", heroID, "biography"));
 
 		// 0xFF is default, 00 male, 01 female
 		hero->gender = static_cast<EHeroGender>(reader->readUInt8());
@@ -1099,7 +1099,7 @@ CGObjectInstance * CMapLoaderH3M::readMonster(const int3 & mapPosition, const Ob
 	bool hasMessage = reader->readBool();
 	if(hasMessage)
 	{
-		object->message = readLocalizedString(TextIdentifier("monster", mapPosition.x, mapPosition.y, mapPosition.z, "message"));
+		object->message.appendTextID(readLocalizedString(TextIdentifier("monster", mapPosition.x, mapPosition.y, mapPosition.z, "message")));
 		reader->readResourses(object->resources);
 		object->gainedArtifact = reader->readArtifact();
 	}
@@ -1135,7 +1135,7 @@ CGObjectInstance * CMapLoaderH3M::readMonster(const int3 & mapPosition, const Ob
 CGObjectInstance * CMapLoaderH3M::readSign(const int3 & mapPosition)
 {
 	auto * object = new CGSignBottle();
-	object->message = readLocalizedString(TextIdentifier("sign", mapPosition.x, mapPosition.y, mapPosition.z, "message"));
+	object->message.appendTextID(readLocalizedString(TextIdentifier("sign", mapPosition.x, mapPosition.y, mapPosition.z, "message")));
 	reader->skipZero(4);
 	return object;
 }
@@ -1685,7 +1685,7 @@ CGObjectInstance * CMapLoaderH3M::readHero(const int3 & mapPosition, const Objec
 	{
 		if(elem.heroId.getNum() == object->subID)
 		{
-			object->nameCustom = elem.name;
+			object->nameCustomTextId = elem.name;
 			object->portrait = elem.portrait;
 			break;
 		}
@@ -1693,7 +1693,7 @@ CGObjectInstance * CMapLoaderH3M::readHero(const int3 & mapPosition, const Objec
 
 	bool hasName = reader->readBool();
 	if(hasName)
-		object->nameCustom = readLocalizedString(TextIdentifier("heroes", object->subID, "name"));
+		object->nameCustomTextId = readLocalizedString(TextIdentifier("heroes", object->subID, "name"));
 
 	if(features.levelSOD)
 	{
@@ -1748,7 +1748,7 @@ CGObjectInstance * CMapLoaderH3M::readHero(const int3 & mapPosition, const Objec
 	{
 		bool hasCustomBiography = reader->readBool();
 		if(hasCustomBiography)
-			object->biographyCustom = readLocalizedString(TextIdentifier("heroes", object->subID, "biography"));
+			object->biographyCustomTextId = readLocalizedString(TextIdentifier("heroes", object->subID, "biography"));
 
 		object->gender = static_cast<EHeroGender>(reader->readUInt8());
 		assert(object->gender == EHeroGender::MALE || object->gender == EHeroGender::FEMALE || object->gender == EHeroGender::DEFAULT);
@@ -2072,9 +2072,9 @@ void CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position)
 	}
 
 	guard->quest->lastDay = reader->readInt32();
-	guard->quest->firstVisitText = readLocalizedString(TextIdentifier("quest", position.x, position.y, position.z, "firstVisit"));
-	guard->quest->nextVisitText = readLocalizedString(TextIdentifier("quest", position.x, position.y, position.z, "nextVisit"));
-	guard->quest->completedText = readLocalizedString(TextIdentifier("quest", position.x, position.y, position.z, "completed"));
+	guard->quest->firstVisitText.appendTextID(readLocalizedString(TextIdentifier("quest", position.x, position.y, position.z, "firstVisit")));
+	guard->quest->nextVisitText.appendTextID(readLocalizedString(TextIdentifier("quest", position.x, position.y, position.z, "nextVisit")));
+	guard->quest->completedText.appendTextID(readLocalizedString(TextIdentifier("quest", position.x, position.y, position.z, "completed")));
 	guard->quest->isCustomFirst = !guard->quest->firstVisitText.empty();
 	guard->quest->isCustomNext = !guard->quest->nextVisitText.empty();
 	guard->quest->isCustomComplete = !guard->quest->completedText.empty();
@@ -2094,7 +2094,7 @@ CGObjectInstance * CMapLoaderH3M::readTown(const int3 & position, std::shared_pt
 
 	bool hasName = reader->readBool();
 	if(hasName)
-		object->setNameTranslated(readLocalizedString(TextIdentifier("town", position.x, position.y, position.z, "name")));
+		object->setNameTextId(readLocalizedString(TextIdentifier("town", position.x, position.y, position.z, "name")));
 
 	bool hasGarrison = reader->readBool();
 	if(hasGarrison)
@@ -2155,7 +2155,7 @@ CGObjectInstance * CMapLoaderH3M::readTown(const int3 & position, std::shared_pt
 	{
 		CCastleEvent event;
 		event.name = readBasicString();
-		event.message = readLocalizedString(TextIdentifier("town", position.x, position.y, position.z, "event", eventID, "description"));
+		event.message.appendTextID(readLocalizedString(TextIdentifier("town", position.x, position.y, position.z, "event", eventID, "description")));
 
 		reader->readResourses(event.resources);
 
@@ -2225,7 +2225,7 @@ void CMapLoaderH3M::readEvents()
 	{
 		CMapEvent event;
 		event.name = readBasicString();
-		event.message = readLocalizedString(TextIdentifier("event", eventID, "description"));
+		event.message.appendTextID(readLocalizedString(TextIdentifier("event", eventID, "description")));
 
 		reader->readResourses(event.resources);
 		event.players = reader->readUInt8();
@@ -2247,12 +2247,12 @@ void CMapLoaderH3M::readEvents()
 	}
 }
 
-void CMapLoaderH3M::readMessageAndGuards(std::string & message, CCreatureSet * guards, const int3 & position)
+void CMapLoaderH3M::readMessageAndGuards(MetaString & message, CCreatureSet * guards, const int3 & position)
 {
 	bool hasMessage = reader->readBool();
 	if(hasMessage)
 	{
-		message = readLocalizedString(TextIdentifier("guards", position.x, position.y, position.z, "message"));
+		message.appendTextID(readLocalizedString(TextIdentifier("guards", position.x, position.y, position.z, "message")));
 		bool hasGuards = reader->readBool();
 		if(hasGuards)
 			readCreatureSet(guards, 7);
@@ -2274,8 +2274,7 @@ std::string CMapLoaderH3M::readLocalizedString(const TextIdentifier & stringIden
 	if(mapString.empty())
 		return "";
 
-	VLC->generaltexth->registerString(modName, fullIdentifier, mapString);
-	return VLC->generaltexth->translate(fullIdentifier.get());
+	return mapRegisterLocalizedString(modName, *mapHeader, fullIdentifier, mapString);
 }
 
 void CMapLoaderH3M::afterRead()

+ 2 - 1
lib/mapping/MapFormatH3M.h

@@ -17,6 +17,7 @@ VCMI_LIB_NAMESPACE_BEGIN
 
 class CGHeroInstance;
 class MapReaderH3M;
+class MetaString;
 class CArtifactInstance;
 class CGObjectInstance;
 class CGSeerHut;
@@ -215,7 +216,7 @@ private:
 	/**
 	* read optional message and optional guards
 	*/
-	void readMessageAndGuards(std::string & message, CCreatureSet * guards, const int3 & position);
+	void readMessageAndGuards(MetaString & message, CCreatureSet * guards, const int3 & position);
 
 	/// reads string from input stream and converts it to unicode
 	std::string readBasicString();

+ 50 - 13
lib/mapping/MapFormatJson.cpp

@@ -35,6 +35,7 @@
 #include "../constants/StringConstants.h"
 #include "../serializer/JsonDeserializer.h"
 #include "../serializer/JsonSerializer.h"
+#include "../Languages.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
@@ -341,11 +342,12 @@ namespace TerrainDetail
 }
 
 ///CMapFormatJson
-const int CMapFormatJson::VERSION_MAJOR = 1;
-const int CMapFormatJson::VERSION_MINOR = 3;
+const int CMapFormatJson::VERSION_MAJOR = 2;
+const int CMapFormatJson::VERSION_MINOR = 0;
 
 const std::string CMapFormatJson::HEADER_FILE_NAME = "header.json";
 const std::string CMapFormatJson::OBJECTS_FILE_NAME = "objects.json";
+const std::string CMapFormatJson::TERRAIN_FILE_NAMES[2] = {"surface_terrain.json", "underground_terrain.json"};
 
 CMapFormatJson::CMapFormatJson():
 	fileVersionMajor(0), fileVersionMinor(0),
@@ -413,8 +415,8 @@ void CMapFormatJson::serializeAllowedFactions(JsonSerializeFormat & handler, std
 
 void CMapFormatJson::serializeHeader(JsonSerializeFormat & handler)
 {
-	handler.serializeString("name", mapHeader->name);
-	handler.serializeString("description", mapHeader->description);
+	handler.serializeStruct("name", mapHeader->name);
+	handler.serializeStruct("description", mapHeader->description);
 	handler.serializeInt("heroLevelLimit", mapHeader->levelLimit, 0);
 
 	//todo: support arbitrary percentage
@@ -424,10 +426,10 @@ void CMapFormatJson::serializeHeader(JsonSerializeFormat & handler)
 
 	handler.serializeLIC("allowedHeroes", &HeroTypeID::decode, &HeroTypeID::encode, VLC->heroh->getDefaultAllowed(), mapHeader->allowedHeroes);
 
-//	handler.serializeString("victoryString", mapHeader->victoryMessage);
+	handler.serializeStruct("victoryMessage", mapHeader->victoryMessage);
 	handler.serializeInt("victoryIconIndex", mapHeader->victoryIconIndex);
 
-//	handler.serializeString("defeatString", mapHeader->defeatMessage);
+	handler.serializeStruct("defeatMessage", mapHeader->defeatMessage);
 	handler.serializeInt("defeatIconIndex", mapHeader->defeatIconIndex);
 }
 
@@ -529,7 +531,7 @@ void CMapFormatJson::serializePlayerInfo(JsonSerializeFormat & handler)
 					if(hero)
 					{
 						auto heroData = handler.enterStruct(hero->instanceName);
-						heroData->serializeString("name", hero->nameCustom);
+						heroData->serializeString("name", hero->nameCustomTextId);
 
 						if(hero->ID == Obj::HERO)
 						{
@@ -571,7 +573,7 @@ void CMapFormatJson::serializePlayerInfo(JsonSerializeFormat & handler)
 				if(instanceName == info.mainHeroInstance)
 				{
 					//this is main hero
-					info.mainCustomHeroName = hname.heroName;
+					info.mainCustomHeroNameTextId = hname.heroName;
 					info.hasRandomHero = (hname.heroId == -1);
 					info.mainCustomHeroId = hname.heroId;
 					info.mainCustomHeroPortrait = -1;
@@ -905,6 +907,11 @@ std::unique_ptr<CMapHeader> CMapLoaderJson::loadMapHeader()
 	return result;
 }
 
+bool CMapLoaderJson::isExistArchive(const std::string & archiveFilename)
+{
+	return loader.existsResource(JsonPath::builtin(archiveFilename));
+}
+
 JsonNode CMapLoaderJson::getFromArchive(const std::string & archiveFilename)
 {
 	JsonPath resource = JsonPath::builtin(archiveFilename);
@@ -937,7 +944,7 @@ void CMapLoaderJson::readHeader(const bool complete)
 
 	fileVersionMajor = static_cast<int>(header["versionMajor"].Integer());
 
-	if(fileVersionMajor != VERSION_MAJOR)
+	if(fileVersionMajor > VERSION_MAJOR)
 	{
 		logGlobal->error("Unsupported map format version: %d", fileVersionMajor);
 		throw std::runtime_error("Unsupported map format version");
@@ -997,6 +1004,8 @@ void CMapLoaderJson::readHeader(const bool complete)
 
 	if(complete)
 		readOptions(handler);
+	
+	readTranslations();
 }
 
 void CMapLoaderJson::readTerrainTile(const std::string & src, TerrainTile & tile)
@@ -1122,12 +1131,12 @@ void CMapLoaderJson::readTerrainLevel(const JsonNode & src, const int index)
 void CMapLoaderJson::readTerrain()
 {
 	{
-		const JsonNode surface = getFromArchive("surface_terrain.json");
+		const JsonNode surface = getFromArchive(TERRAIN_FILE_NAMES[0]);
 		readTerrainLevel(surface, 0);
 	}
 	if(map->twoLevel)
 	{
-		const JsonNode underground = getFromArchive("underground_terrain.json");
+		const JsonNode underground = getFromArchive(TERRAIN_FILE_NAMES[1]);
 		readTerrainLevel(underground, 1);
 	}
 
@@ -1258,6 +1267,18 @@ void CMapLoaderJson::readObjects()
 	});
 }
 
+void CMapLoaderJson::readTranslations()
+{
+	std::list<Languages::Options> languages{Languages::getLanguageList().begin(), Languages::getLanguageList().end()};
+	for(auto & language : Languages::getLanguageList())
+	{
+		if(isExistArchive(language.identifier + ".json"))
+			mapHeader->translations.Struct()[language.identifier] = getFromArchive(language.identifier + ".json");
+	}
+	mapHeader->registerMapStrings();
+}
+
+
 ///CMapSaverJson
 CMapSaverJson::CMapSaverJson(CInputOutputStream * stream)
 	: buffer(stream)
@@ -1340,6 +1361,8 @@ void CMapSaverJson::writeHeader()
 
 	writeOptions(handler);
 
+	writeTranslations();
+
 	addToArchive(header, HEADER_FILE_NAME);
 }
 
@@ -1388,12 +1411,12 @@ void CMapSaverJson::writeTerrain()
 	//todo: multilevel map save support
 
 	JsonNode surface = writeTerrainLevel(0);
-	addToArchive(surface, "surface_terrain.json");
+	addToArchive(surface, TERRAIN_FILE_NAMES[0]);
 
 	if(map->twoLevel)
 	{
 		JsonNode underground = writeTerrainLevel(1);
-		addToArchive(underground, "underground_terrain.json");
+		addToArchive(underground, TERRAIN_FILE_NAMES[1]);
 	}
 }
 
@@ -1439,5 +1462,19 @@ void CMapSaverJson::writeObjects()
 	addToArchive(data, OBJECTS_FILE_NAME);
 }
 
+void CMapSaverJson::writeTranslations()
+{
+	for(auto & s : mapHeader->translations.Struct())
+	{
+		auto & language = s.first;
+		if(Languages::getLanguageOptions(language).identifier.empty())
+		{
+			logGlobal->error("Serializing of unsupported language %s is not permitted", language);
+			continue;;
+		}
+		logGlobal->trace("Saving translations, language: %s", language);
+		addToArchive(s.second, language + ".json");
+	}
+}
 
 VCMI_LIB_NAMESPACE_END

+ 12 - 0
lib/mapping/MapFormatJson.h

@@ -42,6 +42,7 @@ public:
 
 	static const std::string HEADER_FILE_NAME;
 	static const std::string OBJECTS_FILE_NAME;
+	static const std::string TERRAIN_FILE_NAMES[2];
 
 	int fileVersionMajor;
 	int fileVersionMinor;
@@ -201,6 +202,11 @@ public:
 	 * Reads complete map.
 	 */
 	void readMap();
+	
+	/**
+	 * Reads texts and translations
+	 */
+	void readTranslations();
 
 	static void readTerrainTile(const std::string & src, TerrainTile & tile);
 
@@ -213,6 +219,7 @@ public:
 	 */
 	void readObjects();
 
+	bool isExistArchive(const std::string & archiveFilename);
 	JsonNode getFromArchive(const std::string & archiveFilename);
 
 private:
@@ -248,6 +255,11 @@ public:
 	 * Saves header to zip archive
 	 */
 	void writeHeader();
+	
+	/**
+	 * Saves texts and translations to zip archive
+	 */
+	void writeTranslations();
 
 	/**
 	 * Encodes one tile into string

+ 3 - 1
lib/modding/CModHandler.cpp

@@ -319,8 +319,10 @@ TModID CModHandler::findResourceOrigin(const ResourcePath & name)
 
 std::string CModHandler::getModLanguage(const TModID& modId) const
 {
-	if ( modId == "core")
+	if(modId == "core")
 		return VLC->generaltexth->getInstalledLanguage();
+	if(modId == "map")
+		return VLC->generaltexth->getPreferredLanguage();
 	return allMods.at(modId).baseLanguage;
 }
 

+ 2 - 2
lib/rmg/CMapGenerator.cpp

@@ -407,8 +407,8 @@ void CMapGenerator::addHeaderInfo()
 	m.width = mapGenOptions.getWidth();
 	m.height = mapGenOptions.getHeight();
 	m.twoLevel = mapGenOptions.getHasTwoLevels();
-	m.name = VLC->generaltexth->allTexts[740];
-	m.description = getMapDescription();
+	m.name.appendLocalString(EMetaText::GENERAL_TXT, 740);
+	m.description.appendRawString(getMapDescription());
 	m.difficulty = 1;
 	addPlayerInfo();
 	m.waterMap = (mapGenOptions.getWaterContent() != EWaterContent::EWaterContent::NONE);

+ 3 - 0
mapeditor/CMakeLists.txt

@@ -21,6 +21,7 @@ set(editor_SRCS
 		mapsettings/loseconditions.cpp
 		mapsettings/eventsettings.cpp
 		mapsettings/rumorsettings.cpp
+		mapsettings/translations.cpp
 		playersettings.cpp
 		playerparams.cpp
 		scenelayer.cpp
@@ -58,6 +59,7 @@ set(editor_HEADERS
 		mapsettings/loseconditions.h
 		mapsettings/eventsettings.h
 		mapsettings/rumorsettings.h
+		mapsettings/translations.h
 		playersettings.h
 		playerparams.h
 		scenelayer.h
@@ -85,6 +87,7 @@ set(editor_FORMS
 		mapsettings/loseconditions.ui
 		mapsettings/eventsettings.ui
 		mapsettings/rumorsettings.ui
+		mapsettings/translations.ui
 		playersettings.ui
 		playerparams.ui
 		validator.ui

BIN
mapeditor/icons/translations.png


+ 24 - 11
mapeditor/inspector/inspector.cpp

@@ -277,8 +277,8 @@ void Inspector::updateProperties(CGHeroInstance * o)
 		delegate->options = {{"MALE", QVariant::fromValue(int(EHeroGender::MALE))}, {"FEMALE", QVariant::fromValue(int(EHeroGender::FEMALE))}};
 		addProperty<std::string>("Gender", (o->gender == EHeroGender::FEMALE ? "FEMALE" : "MALE"), delegate , false);
 	}
-	addProperty("Name", o->nameCustom, false);
-	addProperty("Biography", o->biographyCustom, new MessageDelegate, false);
+	addProperty("Name", o->getNameTranslated(), false);
+	addProperty("Biography", o->getBiographyTranslated(), new MessageDelegate, false);
 	addProperty("Portrait", o->portrait, false);
 	
 	auto * delegate = new HeroSkillsDelegate(*o);
@@ -531,7 +531,7 @@ void Inspector::setProperty(CGPandoraBox * o, const QString & key, const QVarian
 	if(!o) return;
 	
 	if(key == "Message")
-		o->message = value.toString().toStdString();
+		o->message = MetaString::createFromTextID(mapRegisterLocalizedString("map", *map, TextIdentifier("guards", o->instanceName, "message"), value.toString().toStdString()));
 }
 
 void Inspector::setProperty(CGEvent * o, const QString & key, const QVariant & value)
@@ -553,7 +553,7 @@ void Inspector::setProperty(CGTownInstance * o, const QString & key, const QVari
 	if(!o) return;
 	
 	if(key == "Town name")
-		o->setNameTranslated(value.toString().toStdString());
+		o->setNameTextId(mapRegisterLocalizedString("map", *map, TextIdentifier("town", o->instanceName, "name"), value.toString().toStdString()));
 }
 
 void Inspector::setProperty(CGSignBottle * o, const QString & key, const QVariant & value)
@@ -561,7 +561,7 @@ void Inspector::setProperty(CGSignBottle * o, const QString & key, const QVarian
 	if(!o) return;
 	
 	if(key == "Message")
-		o->message = value.toString().toStdString();
+		o->message = MetaString::createFromTextID(mapRegisterLocalizedString("map", *map, TextIdentifier("sign", o->instanceName, "message"), value.toString().toStdString()));
 }
 
 void Inspector::setProperty(CGMine * o, const QString & key, const QVariant & value)
@@ -577,7 +577,7 @@ void Inspector::setProperty(CGArtifact * o, const QString & key, const QVariant
 	if(!o) return;
 	
 	if(key == "Message")
-		o->message = value.toString().toStdString();
+		o->message = MetaString::createFromTextID(mapRegisterLocalizedString("map", *map, TextIdentifier("guards", o->instanceName, "message"), value.toString().toStdString()));
 	
 	if(o->storedArtifact && key == "Spell")
 	{
@@ -606,7 +606,10 @@ void Inspector::setProperty(CGHeroInstance * o, const QString & key, const QVari
 		o->gender = EHeroGender(value.toInt());
 	
 	if(key == "Name")
-		o->nameCustom = value.toString().toStdString();
+		o->nameCustomTextId = mapRegisterLocalizedString("map", *map, TextIdentifier("hero", o->instanceName, "name"), value.toString().toStdString());
+	
+	if(key == "Biography")
+		o->biographyCustomTextId = mapRegisterLocalizedString("map", *map, TextIdentifier("hero", o->instanceName, "biography"), value.toString().toStdString());
 	
 	if(key == "Experience")
 		o->exp = value.toString().toInt();
@@ -643,7 +646,7 @@ void Inspector::setProperty(CGCreature * o, const QString & key, const QVariant
 	if(!o) return;
 	
 	if(key == "Message")
-		o->message = value.toString().toStdString();
+		o->message = MetaString::createFromTextID(mapRegisterLocalizedString("map", *map, TextIdentifier("monster", o->instanceName, "message"), value.toString().toStdString()));
 	if(key == "Character")
 		o->character = CGCreature::Character(value.toInt());
 	if(key == "Never flees")
@@ -661,11 +664,11 @@ void Inspector::setProperty(CGSeerHut * o, const QString & key, const QVariant &
 	if(key == "Mission type")
 		o->quest->missionType = CQuest::Emission(value.toInt());
 	if(key == "First visit text")
-		o->quest->firstVisitText = value.toString().toStdString();
+		o->quest->firstVisitText = MetaString::createFromTextID(mapRegisterLocalizedString("map", *map, TextIdentifier("quest", o->instanceName, "firstVisit"), value.toString().toStdString()));
 	if(key == "Next visit text")
-		o->quest->nextVisitText = value.toString().toStdString();
+		o->quest->nextVisitText = MetaString::createFromTextID(mapRegisterLocalizedString("map", *map, TextIdentifier("quest", o->instanceName, "nextVisit"), value.toString().toStdString()));
 	if(key == "Completed text")
-		o->quest->completedText = value.toString().toStdString();
+		o->quest->completedText = MetaString::createFromTextID(mapRegisterLocalizedString("map", *map, TextIdentifier("quest", o->instanceName, "completed"), value.toString().toStdString()));
 }
 
 
@@ -713,6 +716,16 @@ QTableWidgetItem * Inspector::addProperty(const std::string & value)
 	return addProperty(QString::fromStdString(value));
 }
 
+QTableWidgetItem * Inspector::addProperty(const TextIdentifier & value)
+{
+	return addProperty(VLC->generaltexth->translate(value.get()));
+}
+
+QTableWidgetItem * Inspector::addProperty(const MetaString & value)
+{
+	return addProperty(value.toString());
+}
+
 QTableWidgetItem * Inspector::addProperty(const QString & value)
 {
 	auto * item = new QTableWidgetItem(value);

+ 5 - 1
mapeditor/inspector/inspector.h

@@ -18,7 +18,9 @@
 #include "../lib/mapObjects/CGCreature.h"
 #include "../lib/mapObjects/MapObjects.h"
 #include "../lib/mapObjects/CRewardableObject.h"
+#include "../lib/CGeneralTextHandler.h"
 #include "../lib/ResourceSet.h"
+#include "../lib/MetaString.h"
 
 #define DECLARE_OBJ_TYPE(x) void initialize(x*);
 #define DECLARE_OBJ_PROPERTY_METHODS(x) \
@@ -83,6 +85,8 @@ protected:
 //===============DECLARE PROPERTY VALUE TYPE==============================
 	QTableWidgetItem * addProperty(unsigned int value);
 	QTableWidgetItem * addProperty(int value);
+	QTableWidgetItem * addProperty(const MetaString & value);
+	QTableWidgetItem * addProperty(const TextIdentifier & value);
 	QTableWidgetItem * addProperty(const std::string & value);
 	QTableWidgetItem * addProperty(const QString & value);
 	QTableWidgetItem * addProperty(const int3 & value);
@@ -144,7 +148,7 @@ protected:
 	{
 		addProperty<T>(key, value, nullptr, restricted);
 	}
-
+	
 protected:
 	int row = 0;
 	QTableWidget * table;

+ 4 - 4
mapeditor/inspector/rewardswidget.cpp

@@ -23,7 +23,7 @@
 #include "../lib/mapObjects/CGPandoraBox.h"
 #include "../lib/mapObjects/CQuest.h"
 
-RewardsWidget::RewardsWidget(const CMap & m, CRewardableObject & p, QWidget *parent) :
+RewardsWidget::RewardsWidget(CMap & m, CRewardableObject & p, QWidget *parent) :
 	QDialog(parent),
 	map(m),
 	object(p),
@@ -211,7 +211,7 @@ bool RewardsWidget::commitChanges()
 	if(ui->onSelectText->text().isEmpty())
 		object.configuration.onSelect.clear();
 	else
-		object.configuration.onSelect = MetaString::createFromRawString(ui->onSelectText->text().toStdString());
+		object.configuration.onSelect = MetaString::createFromTextID(mapRegisterLocalizedString("map", map, TextIdentifier("reward", object.instanceName, "onSelect"), ui->onSelectText->text().toStdString()));
 	object.configuration.canRefuse = ui->canRefuse->isChecked();
 	
 	//reset parameters
@@ -232,7 +232,7 @@ void RewardsWidget::saveCurrentVisitInfo(int index)
 	if(ui->rewardMessage->text().isEmpty())
 		vinfo.message.clear();
 	else
-		vinfo.message = MetaString::createFromRawString(ui->rewardMessage->text().toStdString());
+		vinfo.message = MetaString::createFromTextID(mapRegisterLocalizedString("map", map, TextIdentifier("reward", object.instanceName, "info", index, "message"), ui->rewardMessage->text().toStdString()));
 	
 	vinfo.reward.heroLevel = ui->rHeroLevel->value();
 	vinfo.reward.heroExperience = ui->rHeroExperience->value();
@@ -649,7 +649,7 @@ void RewardsDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, c
 	}
 }
 
-RewardsDelegate::RewardsDelegate(const CMap & m, CRewardableObject & t): map(m), object(t)
+RewardsDelegate::RewardsDelegate(CMap & m, CRewardableObject & t): map(m), object(t)
 {
 }
 

+ 4 - 4
mapeditor/inspector/rewardswidget.h

@@ -22,7 +22,7 @@ class RewardsWidget : public QDialog
 
 public:
 	
-	explicit RewardsWidget(const CMap &, CRewardableObject &, QWidget *parent = nullptr);
+	explicit RewardsWidget(CMap &, CRewardableObject &, QWidget *parent = nullptr);
 	~RewardsWidget();
 	
 	void obtainData();
@@ -64,14 +64,14 @@ private:
 	
 	Ui::RewardsWidget *ui;
 	CRewardableObject & object;
-	const CMap & map;
+	CMap & map;
 };
 
 class RewardsDelegate : public QStyledItemDelegate
 {
 	Q_OBJECT
 public:
-	RewardsDelegate(const CMap &, CRewardableObject &);
+	RewardsDelegate(CMap &, CRewardableObject &);
 
 	using QStyledItemDelegate::QStyledItemDelegate;
 	
@@ -82,5 +82,5 @@ public:
 
 private:
 	CRewardableObject & object;
-	const CMap & map;
+	CMap & map;
 };

+ 68 - 30
mapeditor/mainwindow.cpp

@@ -42,6 +42,7 @@
 #include "objectbrowser.h"
 #include "inspector/inspector.h"
 #include "mapsettings/mapsettings.h"
+#include "mapsettings/translations.h"
 #include "playersettings.h"
 #include "validator.h"
 
@@ -300,6 +301,7 @@ void MainWindow::initializeMap(bool isNew)
 	//enable settings
 	ui->actionMapSettings->setEnabled(true);
 	ui->actionPlayers_settings->setEnabled(true);
+	ui->actionTranslations->setEnabled(true);
 	
 	//set minimal players count
 	if(isNew)
@@ -311,7 +313,7 @@ void MainWindow::initializeMap(bool isNew)
 	onPlayersChanged();
 }
 
-bool MainWindow::openMap(const QString & filenameSelect)
+std::unique_ptr<CMap> MainWindow::openMapInternal(const QString & filenameSelect)
 {
 	QFileInfo fi(filenameSelect);
 	std::string fname = fi.fileName().toStdString();
@@ -325,26 +327,30 @@ bool MainWindow::openMap(const QString & filenameSelect)
 	CResourceHandler::addFilesystem("local", "mapEditor", mapEditorFilesystem);
 	
 	if(!CResourceHandler::get("mapEditor")->existsResource(resId))
-	{
-		QMessageBox::warning(this, tr("Failed to open map"), tr("Cannot open map from this folder"));
-		return false;
-	}
+		throw std::runtime_error("Cannot open map from this folder");
 	
 	CMapService mapService;
+	if(auto header = mapService.loadMapHeader(resId))
+	{
+		auto missingMods = CMapService::verifyMapHeaderMods(*header);
+		ModIncompatibility::ModListWithVersion modList;
+		for(const auto & m : missingMods)
+			modList.push_back({m.second.name, m.second.version.toString()});
+		
+		if(!modList.empty())
+			throw ModIncompatibility(modList);
+		
+		return mapService.loadMap(resId);
+	}
+	else
+		throw std::runtime_error("Corrupted map");
+}
+
+bool MainWindow::openMap(const QString & filenameSelect)
+{
 	try
 	{
-		if(auto header = mapService.loadMapHeader(resId))
-		{
-			auto missingMods = CMapService::verifyMapHeaderMods(*header);
-			ModIncompatibility::ModListWithVersion modList;
-			for(const auto & m : missingMods)
-				modList.push_back({m.second.name, m.second.version.toString()});
-			
-			if(!modList.empty())
-				throw ModIncompatibility(modList);
-			
-			controller.setMap(mapService.loadMap(resId));
-		}
+		controller.setMap(openMapInternal(filenameSelect));
 	}
 	catch(const ModIncompatibility & e)
 	{
@@ -354,7 +360,7 @@ bool MainWindow::openMap(const QString & filenameSelect)
 	}
 	catch(const std::exception & e)
 	{
-		QMessageBox::critical(this, "Failed to open map", e.what());
+		QMessageBox::critical(this, "Failed to open map", tr(e.what()));
 		return false;
 	}
 	
@@ -398,6 +404,8 @@ void MainWindow::saveMap()
 		else
 			QMessageBox::information(this, "Map validation", "Map has some errors. Open Validator from the Map menu to see issues found");
 	}
+	
+	Translations::cleanupRemovedItems(*controller.map());
 
 	CMapService mapService;
 	try
@@ -419,7 +427,7 @@ void MainWindow::on_actionSave_as_triggered()
 	if(!controller.map())
 		return;
 
-	auto filenameSelect = QFileDialog::getSaveFileName(this, tr("Save map"), "", tr("VCMI maps (*.vmap)"));
+	auto filenameSelect = QFileDialog::getSaveFileName(this, tr("Save map"), lastSavingDir, tr("VCMI maps (*.vmap)"));
 
 	if(filenameSelect.isNull())
 		return;
@@ -428,6 +436,7 @@ void MainWindow::on_actionSave_as_triggered()
 		return;
 
 	filename = filenameSelect;
+	lastSavingDir = filenameSelect.remove(QUrl(filenameSelect).fileName());
 
 	saveMap();
 }
@@ -445,16 +454,9 @@ void MainWindow::on_actionSave_triggered()
 		return;
 
 	if(filename.isNull())
-	{
-		auto filenameSelect = QFileDialog::getSaveFileName(this, tr("Save map"), "", tr("VCMI maps (*.vmap)"));
-
-		if(filenameSelect.isNull())
-			return;
-
-		filename = filenameSelect;
-	}
-
-	saveMap();
+		on_actionSave_as_triggered();
+	else
+		saveMap();
 }
 
 void MainWindow::terrainButtonClicked(TerrainId terrain)
@@ -1229,7 +1231,7 @@ void MainWindow::on_actionPaste_triggered()
 
 void MainWindow::on_actionExport_triggered()
 {
-	QString fileName = QFileDialog::getSaveFileName(this, tr("Save to image"), QCoreApplication::applicationDirPath(), "BMP (*.bmp);;JPEG (*.jpeg);;PNG (*.png)");
+	QString fileName = QFileDialog::getSaveFileName(this, tr("Save to image"), lastSavingDir, "BMP (*.bmp);;JPEG (*.jpeg);;PNG (*.png)");
 	if(!fileName.isNull())
 	{
 		QImage image(ui->mapView->scene()->sceneRect().size().toSize(), QImage::Format_RGB888);
@@ -1239,3 +1241,39 @@ void MainWindow::on_actionExport_triggered()
 	}
 }
 
+
+void MainWindow::on_actionTranslations_triggered()
+{
+	auto translationsDialog = new Translations(*controller.map(), this);
+	translationsDialog->show();
+}
+
+void MainWindow::on_actionh3m_converter_triggered()
+{
+	auto mapFiles = QFileDialog::getOpenFileNames(this, tr("Select maps to convert"),
+		QString::fromStdString(VCMIDirs::get().userCachePath().make_preferred().string()),
+		tr("HoMM3 maps(*.h3m)"));
+	if(mapFiles.empty())
+		return;
+	
+	auto saveDirectory = QFileDialog::getExistingDirectory(this, tr("Choose directory to save converted maps"), QCoreApplication::applicationDirPath());
+	if(saveDirectory.isEmpty())
+		return;
+	
+	try
+	{
+		for(auto & m : mapFiles)
+		{
+			CMapService mapService;
+			auto map = openMapInternal(m);
+			controller.repairMap(map.get());
+			mapService.saveMap(map, (saveDirectory + '/' + QFileInfo(m).completeBaseName() + ".vmap").toStdString());
+		}
+		QMessageBox::information(this, tr("Operation completed"), tr("Successfully converted %1 maps").arg(mapFiles.size()));
+	}
+	catch(const std::exception & e)
+	{
+		QMessageBox::critical(this, tr("Failed to convert the map. Abort operation"), tr(e.what()));
+	}
+}
+

+ 7 - 1
mapeditor/mainwindow.h

@@ -31,6 +31,8 @@ class MainWindow : public QMainWindow
 #ifdef ENABLE_QT_TRANSLATIONS
 	QTranslator translator;
 #endif
+	
+	std::unique_ptr<CMap> openMapInternal(const QString &);
 
 public:
     explicit MainWindow(QWidget *parent = nullptr);
@@ -117,6 +119,10 @@ private slots:
 
 	void on_actionExport_triggered();
 
+	void on_actionTranslations_triggered();
+	
+	void on_actionh3m_converter_triggered();
+
 public slots:
 
 	void treeViewSelected(const QModelIndex &selected, const QModelIndex &deselected);
@@ -153,7 +159,7 @@ private:
 	ObjectBrowserProxyModel * objectBrowser = nullptr;
 	QGraphicsScene * scenePreview;
 	
-	QString filename;
+	QString filename, lastSavingDir;
 	bool unsaved = false;
 
 	QStandardItemModel objectsModel;

+ 121 - 17
mapeditor/mainwindow.ui

@@ -14,20 +14,20 @@
    <string>VCMI Map Editor</string>
   </property>
   <widget class="QWidget" name="centralwidget">
-   <layout class="QGridLayout" name="gridLayout">
+   <layout class="QVBoxLayout" name="verticalLayout_12">
     <property name="leftMargin">
-     <number>2</number>
+     <number>0</number>
     </property>
     <property name="topMargin">
-     <number>2</number>
+     <number>0</number>
     </property>
     <property name="rightMargin">
-     <number>2</number>
+     <number>0</number>
     </property>
     <property name="bottomMargin">
-     <number>2</number>
+     <number>0</number>
     </property>
-    <item row="0" column="0">
+    <item>
      <widget class="MapView" name="mapView">
       <property name="sizePolicy">
        <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
@@ -51,7 +51,7 @@
      <x>0</x>
      <y>0</y>
      <width>1024</width>
-     <height>22</height>
+     <height>37</height>
     </rect>
    </property>
    <widget class="QMenu" name="menuFile">
@@ -63,6 +63,7 @@
     <addaction name="actionSave"/>
     <addaction name="actionSave_as"/>
     <addaction name="actionExport"/>
+    <addaction name="actionh3m_converter"/>
    </widget>
    <widget class="QMenu" name="menuMap">
     <property name="title">
@@ -70,6 +71,7 @@
     </property>
     <addaction name="actionMapSettings"/>
     <addaction name="actionPlayers_settings"/>
+    <addaction name="actionTranslations"/>
     <addaction name="actionValidate"/>
     <addaction name="actionUpdate_appearance"/>
     <addaction name="actionRecreate_obstacles"/>
@@ -140,6 +142,7 @@
    <addaction name="actionPaste"/>
    <addaction name="separator"/>
    <addaction name="actionFill"/>
+   <addaction name="actionTranslations"/>
   </widget>
   <widget class="QDockWidget" name="dockWidget_2">
    <property name="sizePolicy">
@@ -156,7 +159,7 @@
    </property>
    <property name="maximumSize">
     <size>
-     <width>192</width>
+     <width>524287</width>
      <height>214</height>
     </size>
    </property>
@@ -219,7 +222,7 @@
   </widget>
   <widget class="QDockWidget" name="dockWidget_3">
    <property name="sizePolicy">
-    <sizepolicy hsizetype="Preferred" vsizetype="Expanding">
+    <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
      <horstretch>0</horstretch>
      <verstretch>0</verstretch>
     </sizepolicy>
@@ -244,7 +247,7 @@
    </attribute>
    <widget class="QWidget" name="dockWidgetContents_3">
     <property name="sizePolicy">
-     <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
+     <sizepolicy hsizetype="MinimumExpanding" vsizetype="Expanding">
       <horstretch>0</horstretch>
       <verstretch>0</verstretch>
      </sizepolicy>
@@ -271,7 +274,7 @@
         </sizepolicy>
        </property>
        <property name="currentIndex">
-        <number>0</number>
+        <number>1</number>
        </property>
        <widget class="QWidget" name="tab_2">
         <property name="sizePolicy">
@@ -429,11 +432,17 @@
    <property name="minimumSize">
     <size>
      <width>128</width>
-     <height>496</height>
+     <height>192</height>
+    </size>
+   </property>
+   <property name="maximumSize">
+    <size>
+     <width>524287</width>
+     <height>192</height>
     </size>
    </property>
    <property name="windowTitle">
-    <string>Terrains View</string>
+    <string>Tools</string>
    </property>
    <attribute name="dockWidgetArea">
     <number>1</number>
@@ -473,7 +482,7 @@
      <item>
       <widget class="QGroupBox" name="groupBox">
        <property name="sizePolicy">
-        <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+        <sizepolicy hsizetype="Expanding" vsizetype="Minimum">
          <horstretch>0</horstretch>
          <verstretch>0</verstretch>
         </sizepolicy>
@@ -488,6 +497,18 @@
         <string>Brush</string>
        </property>
        <layout class="QFormLayout" name="formLayout">
+        <property name="leftMargin">
+         <number>0</number>
+        </property>
+        <property name="topMargin">
+         <number>0</number>
+        </property>
+        <property name="rightMargin">
+         <number>0</number>
+        </property>
+        <property name="bottomMargin">
+         <number>0</number>
+        </property>
         <item row="0" column="0">
          <widget class="QPushButton" name="toolBrush">
           <property name="sizePolicy">
@@ -731,6 +752,36 @@
        </layout>
       </widget>
      </item>
+    </layout>
+   </widget>
+  </widget>
+  <widget class="QDockWidget" name="dockWidget_4">
+   <property name="sizePolicy">
+    <sizepolicy hsizetype="Preferred" vsizetype="Expanding">
+     <horstretch>0</horstretch>
+     <verstretch>0</verstretch>
+    </sizepolicy>
+   </property>
+   <property name="windowTitle">
+    <string>Painting</string>
+   </property>
+   <attribute name="dockWidgetArea">
+    <number>1</number>
+   </attribute>
+   <widget class="QWidget" name="dockWidgetContents_6">
+    <layout class="QVBoxLayout" name="verticalLayout_9">
+     <property name="leftMargin">
+      <number>0</number>
+     </property>
+     <property name="topMargin">
+      <number>0</number>
+     </property>
+     <property name="rightMargin">
+      <number>0</number>
+     </property>
+     <property name="bottomMargin">
+      <number>0</number>
+     </property>
      <item>
       <widget class="QToolBox" name="toolBox">
        <property name="sizePolicy">
@@ -754,7 +805,7 @@
           <x>0</x>
           <y>0</y>
           <width>128</width>
-          <height>251</height>
+          <height>192</height>
          </rect>
         </property>
         <property name="sizePolicy">
@@ -797,7 +848,7 @@
           <x>0</x>
           <y>0</y>
           <width>128</width>
-          <height>251</height>
+          <height>192</height>
          </rect>
         </property>
         <property name="sizePolicy">
@@ -833,7 +884,7 @@
           <x>0</x>
           <y>0</y>
           <width>128</width>
-          <height>251</height>
+          <height>192</height>
          </rect>
         </property>
         <property name="sizePolicy">
@@ -865,6 +916,36 @@
        </widget>
       </widget>
      </item>
+    </layout>
+   </widget>
+  </widget>
+  <widget class="QDockWidget" name="dockWidget_5">
+   <property name="maximumSize">
+    <size>
+     <width>524287</width>
+     <height>150</height>
+    </size>
+   </property>
+   <property name="windowTitle">
+    <string>Preview</string>
+   </property>
+   <attribute name="dockWidgetArea">
+    <number>1</number>
+   </attribute>
+   <widget class="QWidget" name="dockWidgetContents_7">
+    <layout class="QVBoxLayout" name="verticalLayout_11">
+     <property name="leftMargin">
+      <number>0</number>
+     </property>
+     <property name="topMargin">
+      <number>0</number>
+     </property>
+     <property name="rightMargin">
+      <number>0</number>
+     </property>
+     <property name="bottomMargin">
+      <number>0</number>
+     </property>
      <item>
       <widget class="QGraphicsView" name="objectPreview">
        <property name="minimumSize">
@@ -1237,6 +1318,29 @@
     <string>Export as...</string>
    </property>
   </action>
+  <action name="actionTranslations">
+   <property name="enabled">
+    <bool>false</bool>
+   </property>
+   <property name="icon">
+    <iconset>
+     <normaloff>icons:translations.png</normaloff>icons:translations.png</iconset>
+   </property>
+   <property name="text">
+    <string>Translations</string>
+   </property>
+   <property name="shortcut">
+    <string>Ctrl+T</string>
+   </property>
+  </action>
+  <action name="actionh3m_converter">
+   <property name="text">
+    <string>h3m converter</string>
+   </property>
+   <property name="toolTip">
+    <string>h3m converter</string>
+   </property>
+  </action>
  </widget>
  <customwidgets>
   <customwidget>

+ 38 - 14
mapeditor/mapcontroller.cpp

@@ -86,26 +86,46 @@ MinimapScene * MapController::miniScene(int level)
 
 void MapController::repairMap()
 {
+	repairMap(map());
+}
+
+void MapController::repairMap(CMap * map) const
+{
+	if(!map)
+		return;
+	
 	//there might be extra skills, arts and spells not imported from map
-	if(VLC->skillh->getDefaultAllowed().size() > map()->allowedAbilities.size())
+	if(VLC->skillh->getDefaultAllowed().size() > map->allowedAbilities.size())
 	{
-		map()->allowedAbilities.resize(VLC->skillh->getDefaultAllowed().size());
+		map->allowedAbilities.resize(VLC->skillh->getDefaultAllowed().size());
 	}
-	if(VLC->arth->getDefaultAllowed().size() > map()->allowedArtifact.size())
+	if(VLC->arth->getDefaultAllowed().size() > map->allowedArtifact.size())
 	{
-		map()->allowedArtifact.resize(VLC->arth->getDefaultAllowed().size());
+		map->allowedArtifact.resize(VLC->arth->getDefaultAllowed().size());
 	}
-	if(VLC->spellh->getDefaultAllowed().size() > map()->allowedSpells.size())
+	if(VLC->spellh->getDefaultAllowed().size() > map->allowedSpells.size())
 	{
-		map()->allowedSpells.resize(VLC->spellh->getDefaultAllowed().size());
+		map->allowedSpells.resize(VLC->spellh->getDefaultAllowed().size());
 	}
-	if(VLC->heroh->getDefaultAllowed().size() > map()->allowedHeroes.size())
+	if(VLC->heroh->getDefaultAllowed().size() > map->allowedHeroes.size())
 	{
-		map()->allowedHeroes.resize(VLC->heroh->getDefaultAllowed().size());
+		map->allowedHeroes.resize(VLC->heroh->getDefaultAllowed().size());
 	}
 	
+	//make sure events/rumors has name to have proper identifiers
+	int emptyNameId = 1;
+	for(auto & e : map->events)
+		if(e.name.empty())
+			e.name = "event_" + std::to_string(emptyNameId++);
+	emptyNameId = 1;
+	for(auto & e : map->rumors)
+		if(e.name.empty())
+			e.name = "rumor_" + std::to_string(emptyNameId++);
+	
 	//fix owners for objects
-	for(auto obj : _map->objects)
+	auto allImpactedObjects(map->objects);
+	allImpactedObjects.insert(allImpactedObjects.end(), map->predefinedHeroes.begin(), map->predefinedHeroes.end());
+	for(auto obj : allImpactedObjects)
 	{
 		//setup proper names (hero name will be fixed later
 		if(obj->ID != Obj::HERO && obj->ID != Obj::PRISON && (obj->typeName.empty() || obj->subTypeName.empty()))
@@ -129,7 +149,7 @@ void MapController::repairMap()
 		//fix hero instance
 		if(auto * nih = dynamic_cast<CGHeroInstance*>(obj.get()))
 		{
-			map()->allowedHeroes.at(nih->subID) = true;
+			map->allowedHeroes.at(nih->subID) = true;
 			auto type = VLC->heroh->objects[nih->subID];
 			assert(type->heroClass);
 			//TODO: find a way to get proper type name
@@ -154,12 +174,16 @@ void MapController::repairMap()
 			if(nih->spellbookContainsSpell(SpellID::PRESET))
 			{
 				nih->removeSpellFromSpellbook(SpellID::PRESET);
-			}
-			else
-			{
 				for(auto spellID : type->spells)
 					nih->addSpellToSpellbook(spellID);
 			}
+			if(nih->spellbookContainsSpell(SpellID::SPELLBOOK_PRESET))
+			{
+				nih->removeSpellFromSpellbook(SpellID::SPELLBOOK_PRESET);
+				if(!nih->getArt(ArtifactPosition::SPELLBOOK) && type->haveSpellBook)
+					nih->putArtifact(ArtifactPosition::SPELLBOOK, ArtifactUtils::createNewArtifactInstance(ArtifactID::SPELLBOOK));
+			}
+			
 			//fix portrait
 			if(nih->portrait < 0 || nih->portrait == 255)
 				nih->portrait = type->imageIndex;
@@ -196,7 +220,7 @@ void MapController::repairMap()
 				art->storedArtifact = a;
 			}
 			else
-				map()->allowedArtifact.at(art->subID) = true;
+				map->allowedArtifact.at(art->subID) = true;
 		}
 	}
 }

+ 2 - 1
mapeditor/mapcontroller.h

@@ -30,8 +30,9 @@ public:
 	~MapController();
 	
 	void setMap(std::unique_ptr<CMap>);
-	void initObstaclePainters(CMap* map);
+	void initObstaclePainters(CMap * map);
 	
+	void repairMap(CMap * map) const;
 	void repairMap();
 	
 	const std::unique_ptr<CMap> & getMapUniquePtr() const; //to be used for map saving

+ 4 - 4
mapeditor/mapsettings/eventsettings.cpp

@@ -37,7 +37,7 @@ QVariant toVariant(const CMapEvent & event)
 {
 	QVariantMap result;
 	result["name"] = QString::fromStdString(event.name);
-	result["message"] = QString::fromStdString(event.message);
+	result["message"] = QString::fromStdString(event.message.toString());
 	result["players"] = QVariant::fromValue(event.players);
 	result["humanAffected"] = QVariant::fromValue(event.humanAffected);
 	result["computerAffected"] = QVariant::fromValue(event.computerAffected);
@@ -47,12 +47,12 @@ QVariant toVariant(const CMapEvent & event)
 	return QVariant(result);
 }
 
-CMapEvent eventFromVariant(const QVariant & variant)
+CMapEvent eventFromVariant(CMapHeader & mapHeader, const QVariant & variant)
 {
 	CMapEvent result;
 	auto v = variant.toMap();
 	result.name = v.value("name").toString().toStdString();
-	result.message = v.value("message").toString().toStdString();
+	result.message.appendTextID(mapRegisterLocalizedString("map", mapHeader, TextIdentifier("header", "event", result.name, "message"), v.value("message").toString().toStdString()));
 	result.players = v.value("players").toInt();
 	result.humanAffected = v.value("humanAffected").toInt();
 	result.computerAffected = v.value("computerAffected").toInt();
@@ -91,7 +91,7 @@ void EventSettings::update()
 	for(int i = 0; i < ui->eventsList->count(); ++i)
 	{
 		const auto * item = ui->eventsList->item(i);
-		controller->map()->events.push_back(eventFromVariant(item->data(Qt::UserRole)));
+		controller->map()->events.push_back(eventFromVariant(*controller->map(), item->data(Qt::UserRole)));
 	}
 }
 

+ 4 - 4
mapeditor/mapsettings/generalsettings.cpp

@@ -27,8 +27,8 @@ GeneralSettings::~GeneralSettings()
 void GeneralSettings::initialize(MapController & c)
 {
 	AbstractSettings::initialize(c);
-	ui->mapNameEdit->setText(tr(controller->map()->name.c_str()));
-	ui->mapDescriptionEdit->setPlainText(tr(controller->map()->description.c_str()));
+	ui->mapNameEdit->setText(QString::fromStdString(controller->map()->name.toString()));
+	ui->mapDescriptionEdit->setPlainText(QString::fromStdString(controller->map()->description.toString()));
 	ui->heroLevelLimit->setValue(controller->map()->levelLimit);
 	ui->heroLevelLimitCheck->setChecked(controller->map()->levelLimit);
 
@@ -59,8 +59,8 @@ void GeneralSettings::initialize(MapController & c)
 
 void GeneralSettings::update()
 {
-	controller->map()->name = ui->mapNameEdit->text().toStdString();
-	controller->map()->description = ui->mapDescriptionEdit->toPlainText().toStdString();
+	controller->map()->name = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller->map(), TextIdentifier("header", "name"),  ui->mapNameEdit->text().toStdString()));
+	controller->map()->description = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller->map(), TextIdentifier("header", "description"), ui->mapDescriptionEdit->toPlainText().toStdString()));
 	if(ui->heroLevelLimitCheck->isChecked())
 		controller->map()->levelLimit = ui->heroLevelLimit->value();
 	else

+ 2 - 2
mapeditor/mapsettings/loseconditions.cpp

@@ -243,7 +243,7 @@ void LoseConditions::on_loseComboBox_currentIndexChanged(int index)
 			loseTypeWidget = new QComboBox;
 			ui->loseParamsLayout->addWidget(loseTypeWidget);
 			for(int i : getObjectIndexes<const CGTownInstance>(*controller->map()))
-				loseTypeWidget->addItem(tr(getTownName(*controller->map(), i).c_str()), QVariant::fromValue(i));
+				loseTypeWidget->addItem(QString::fromStdString(getTownName(*controller->map(), i).c_str()), QVariant::fromValue(i));
 			pickObjectButton = new QToolButton;
 			connect(pickObjectButton, &QToolButton::clicked, this, &LoseConditions::onObjectSelect);
 			ui->loseParamsLayout->addWidget(pickObjectButton);
@@ -254,7 +254,7 @@ void LoseConditions::on_loseComboBox_currentIndexChanged(int index)
 			loseTypeWidget = new QComboBox;
 			ui->loseParamsLayout->addWidget(loseTypeWidget);
 			for(int i : getObjectIndexes<const CGHeroInstance>(*controller->map()))
-				loseTypeWidget->addItem(tr(getHeroName(*controller->map(), i).c_str()), QVariant::fromValue(i));
+				loseTypeWidget->addItem(QString::fromStdString(getHeroName(*controller->map(), i).c_str()), QVariant::fromValue(i));
 			pickObjectButton = new QToolButton;
 			connect(pickObjectButton, &QToolButton::clicked, this, &LoseConditions::onObjectSelect);
 			ui->loseParamsLayout->addWidget(pickObjectButton);

+ 2 - 2
mapeditor/mapsettings/rumorsettings.cpp

@@ -30,7 +30,7 @@ void RumorSettings::initialize(MapController & c)
 	for(auto & rumor : controller->map()->rumors)
 	{
 		auto * item = new QListWidgetItem(QString::fromStdString(rumor.name));
-		item->setData(Qt::UserRole, QVariant(QString::fromStdString(rumor.text)));
+		item->setData(Qt::UserRole, QVariant(QString::fromStdString(rumor.text.toString())));
 		item->setFlags(item->flags() | Qt::ItemIsEditable);
 		ui->rumors->addItem(item);
 	}
@@ -43,7 +43,7 @@ void RumorSettings::update()
 	{
 		Rumor rumor;
 		rumor.name = ui->rumors->item(i)->text().toStdString();
-		rumor.text = ui->rumors->item(i)->data(Qt::UserRole).toString().toStdString();
+		rumor.text.appendTextID(mapRegisterLocalizedString("map", *controller->map(), TextIdentifier("header", "rumor", i, "text"), ui->rumors->item(i)->data(Qt::UserRole).toString().toStdString()));
 		controller->map()->rumors.push_back(rumor);
 	}
 }

+ 198 - 0
mapeditor/mapsettings/translations.cpp

@@ -0,0 +1,198 @@
+/*
+ * translations.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+
+#include "StdInc.h"
+#include "translations.h"
+#include "ui_translations.h"
+#include "../../lib/Languages.h"
+#include "../../lib/CGeneralTextHandler.h"
+#include "../../lib/VCMI_Lib.h"
+
+void Translations::cleanupRemovedItems(CMap & map)
+{
+	std::set<std::string> existingObjects;
+	for(auto object : map.objects)
+		existingObjects.insert(object->instanceName);
+	
+	for(auto & translations : map.translations.Struct())
+	{
+		auto updateTranslations = JsonNode(JsonNode::JsonType::DATA_STRUCT);
+		for(auto & s : translations.second.Struct())
+		{
+			for(auto part : QString::fromStdString(s.first).split('.'))
+			{
+				if(part == "map" || existingObjects.count(part.toStdString()))
+				{
+					updateTranslations.Struct()[s.first] = s.second;
+					break;
+				}
+			}
+		}
+		translations.second = updateTranslations;
+	}
+}
+
+void Translations::cleanupRemovedItems(CMap & map, const std::string & pattern)
+{
+	for(auto & translations : map.translations.Struct())
+	{
+		auto updateTranslations = JsonNode(JsonNode::JsonType::DATA_STRUCT);
+		for(auto & s : translations.second.Struct())
+		{
+			if(s.first.find(pattern) == std::string::npos)
+				updateTranslations.Struct()[s.first] = s.second;
+		}
+		translations.second = updateTranslations;
+	}
+}
+
+Translations::Translations(CMapHeader & mh, QWidget *parent) :
+	QDialog(parent),
+	ui(new Ui::Translations),
+	mapHeader(mh)
+{
+	setAttribute(Qt::WA_DeleteOnClose, true);
+	ui->setupUi(this);
+	
+	//fill languages list
+	std::set<int> indexFoundLang;
+	int foundLang = -1;
+	ui->languageSelect->blockSignals(true);
+	for(auto & language : Languages::getLanguageList())
+	{
+		ui->languageSelect->addItem(QString("%1 (%2)").arg(QString::fromStdString(language.nameEnglish), QString::fromStdString(language.nameNative)));
+		ui->languageSelect->setItemData(ui->languageSelect->count() - 1, QVariant(QString::fromStdString(language.identifier)));
+		if(mapHeader.translations.Struct().count(language.identifier) && !mapHeader.translations[language.identifier].Struct().empty())
+			indexFoundLang.insert(ui->languageSelect->count() - 1);
+		if(language.identifier == VLC->generaltexth->getPreferredLanguage())
+			foundLang = ui->languageSelect->count() - 1;
+	}
+	ui->languageSelect->blockSignals(false);
+	
+	if(foundLang >= 0 && !indexFoundLang.empty() && !indexFoundLang.count(foundLang))
+	{
+		foundLang = *indexFoundLang.begin();
+		mapPreferredLanguage = ui->languageSelect->itemData(foundLang).toString().toStdString();
+	}
+	
+	if(foundLang >= 0)
+		ui->languageSelect->setCurrentIndex(foundLang);
+	
+	if(mapPreferredLanguage.empty())
+		mapPreferredLanguage = VLC->generaltexth->getPreferredLanguage();
+}
+
+Translations::~Translations()
+{
+	mapHeader.registerMapStrings();
+	delete ui;
+}
+
+void Translations::fillTranslationsTable(const std::string & language)
+{
+	Translations::cleanupRemovedItems(dynamic_cast<CMap&>(mapHeader));
+	auto & translation = mapHeader.translations[language];
+	ui->translationsTable->blockSignals(true);
+	ui->translationsTable->setRowCount(0);
+	ui->translationsTable->setRowCount(translation.Struct().size());
+	int i = 0;
+	for(auto & s : translation.Struct())
+	{
+		auto textLines = QString::fromStdString(s.second.String());
+		textLines = textLines.replace('\n', "\\n");
+		
+		auto * wId = new QTableWidgetItem(QString::fromStdString(s.first));
+		auto * wText = new QTableWidgetItem(textLines);
+		wId->setFlags(wId->flags() & ~Qt::ItemIsEditable);
+		wText->setFlags(wId->flags() | Qt::ItemIsEditable);
+		ui->translationsTable->setItem(i, 0, wId);
+		ui->translationsTable->setItem(i++, 1, wText);
+	}
+	ui->translationsTable->resizeColumnToContents(0);
+	ui->translationsTable->blockSignals(false);
+}
+
+void Translations::on_languageSelect_currentIndexChanged(int index)
+{
+	auto language = ui->languageSelect->currentData().toString().toStdString();
+	bool hasLanguage = mapHeader.translations.Struct().count(language);
+	ui->supportedCheck->blockSignals(true);
+	ui->supportedCheck->setChecked(hasLanguage);
+	ui->supportedCheck->blockSignals(false);
+	ui->translationsTable->setEnabled(hasLanguage);
+	if(hasLanguage)
+		fillTranslationsTable(language);
+	else
+		ui->translationsTable->setRowCount(0);
+}
+
+
+void Translations::on_supportedCheck_toggled(bool checked)
+{
+	auto language = ui->languageSelect->currentData().toString().toStdString();
+	auto & translation = mapHeader.translations[language];
+	bool hasRecord = !translation.Struct().empty();
+	
+	if(checked)
+	{
+		//copy from default language
+		translation = mapHeader.translations[mapPreferredLanguage];
+		
+		fillTranslationsTable(language);
+		ui->translationsTable->setEnabled(true);
+	}
+	else
+	{
+		bool canRemove = language != mapPreferredLanguage;
+		if(!canRemove)
+		{
+			QMessageBox::information(this, tr("Remove translation"), tr("Default language cannot be removed"));
+		}
+		else if(hasRecord)
+		{
+			auto sure = QMessageBox::question(this, tr("Remove translation"), tr("All existing text records for this language will be removed. Continue?"));
+			canRemove = sure != QMessageBox::No;
+		}
+		
+		if(!canRemove)
+		{
+			ui->supportedCheck->blockSignals(true);
+			ui->supportedCheck->setChecked(true);
+			ui->supportedCheck->blockSignals(false);
+			return;
+		}
+		ui->translationsTable->blockSignals(true);
+		ui->translationsTable->setRowCount(0);
+		translation = JsonNode(JsonNode::JsonType::DATA_NULL);
+		ui->translationsTable->blockSignals(false);
+		ui->translationsTable->setEnabled(false);
+	}
+}
+
+
+void Translations::on_translationsTable_itemChanged(QTableWidgetItem * item)
+{
+	assert(item->column() == 1);
+	
+	auto language = ui->languageSelect->currentData().toString().toStdString();
+	auto & translation = mapHeader.translations[language];
+	
+	assert(!translation.isNull());
+	
+	auto textId = ui->translationsTable->item(item->row(), 0)->text().toStdString();
+	assert(!textId.empty());
+	if(textId.empty())
+		return;
+	
+	auto textLines = item->text();
+	textLines = textLines.replace("\\n", "\n");
+	translation[textId].String() = textLines.toStdString();
+}
+

+ 45 - 0
mapeditor/mapsettings/translations.h

@@ -0,0 +1,45 @@
+/*
+ * translations.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+
+#pragma once
+
+#include <QDialog>
+#include "../lib/mapping/CMap.h"
+
+namespace Ui {
+class Translations;
+}
+
+class Translations : public QDialog
+{
+	Q_OBJECT
+	
+	void fillTranslationsTable(const std::string & language);
+
+public:
+	explicit Translations(CMapHeader & mapHeader, QWidget *parent = nullptr);
+	~Translations();
+	
+	//removes unused string IDs from map translations
+	static void cleanupRemovedItems(CMap & map);
+	static void cleanupRemovedItems(CMap & map, const std::string & pattern);
+
+private slots:
+	void on_languageSelect_currentIndexChanged(int index);
+
+	void on_supportedCheck_toggled(bool checked);
+
+	void on_translationsTable_itemChanged(QTableWidgetItem *item);
+
+private:
+	Ui::Translations *ui;
+	CMapHeader & mapHeader;
+	std::string mapPreferredLanguage;
+};

+ 84 - 0
mapeditor/mapsettings/translations.ui

@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>Translations</class>
+ <widget class="QDialog" name="Translations">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>989</width>
+    <height>641</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Map translations</string>
+  </property>
+  <property name="modal">
+   <bool>true</bool>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout">
+     <item>
+      <widget class="QLabel" name="label">
+       <property name="sizePolicy">
+        <sizepolicy hsizetype="Minimum" vsizetype="Preferred">
+         <horstretch>0</horstretch>
+         <verstretch>0</verstretch>
+        </sizepolicy>
+       </property>
+       <property name="text">
+        <string>Language</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QComboBox" name="languageSelect">
+       <property name="sizePolicy">
+        <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+         <horstretch>0</horstretch>
+         <verstretch>0</verstretch>
+        </sizepolicy>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QCheckBox" name="supportedCheck">
+       <property name="text">
+        <string>Suppported</string>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <widget class="QTableWidget" name="translationsTable">
+     <attribute name="horizontalHeaderDefaultSectionSize">
+      <number>240</number>
+     </attribute>
+     <attribute name="horizontalHeaderStretchLastSection">
+      <bool>true</bool>
+     </attribute>
+     <attribute name="verticalHeaderVisible">
+      <bool>false</bool>
+     </attribute>
+     <attribute name="verticalHeaderDefaultSectionSize">
+      <number>24</number>
+     </attribute>
+     <column>
+      <property name="text">
+       <string>String ID</string>
+      </property>
+     </column>
+     <column>
+      <property name="text">
+       <string>Text</string>
+      </property>
+     </column>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>

+ 5 - 0
mapeditor/windownewmap.cpp

@@ -90,6 +90,11 @@ void WindowNewMap::loadUserSettings()
 	{
 		ui->heightTxt->setText(height.toString());
 	}
+	for(auto & sz : mapSizes)
+	{
+		if(sz.second.first == width.toInt() && sz.second.second == height.toInt())
+			ui->sizeCombo->setCurrentIndex(sz.first);
+	}
 	auto twoLevel = s.value(newMapTwoLevel);
 	if (twoLevel.isValid())
 	{

+ 2 - 2
server/CGameHandler.cpp

@@ -3221,7 +3221,7 @@ void CGameHandler::handleTimeEvents()
 				//prepare dialog
 				InfoWindow iw;
 				iw.player = color;
-				iw.text.appendRawString(ev.message);
+				iw.text = ev.message;
 
 				for (int i=0; i<ev.resources.size(); i++)
 				{
@@ -3272,7 +3272,7 @@ void CGameHandler::handleTownEvents(CGTownInstance * town, NewTurn &n)
 			// dialog
 			InfoWindow iw;
 			iw.player = player;
-			iw.text.appendRawString(ev.message);
+			iw.text = ev.message;
 
 			if (ev.resources.nonZero())
 			{

+ 1 - 1
server/CVCMIServer.cpp

@@ -730,7 +730,7 @@ void CVCMIServer::updateStartInfoOnMapChange(std::shared_ptr<CMapInfo> mapInfo,
 			if(pset.hero.getNum() != PlayerSettings::RANDOM && pinfo.hasCustomMainHero())
 			{
 				pset.hero = pinfo.mainCustomHeroId;
-				pset.heroName = pinfo.mainCustomHeroName;
+				pset.heroNameTextId = pinfo.mainCustomHeroNameTextId;
 				pset.heroPortrait = pinfo.mainCustomHeroPortrait;
 			}
 

+ 1 - 1
test/game/CGameStateTest.cpp

@@ -170,7 +170,7 @@ public:
 			if(pset.hero.getNum() != PlayerSettings::RANDOM && pinfo.hasCustomMainHero())
 			{
 				pset.hero = pinfo.mainCustomHeroId;
-				pset.heroName = pinfo.mainCustomHeroName;
+				pset.heroNameTextId = pinfo.mainCustomHeroNameTextId;
 				pset.heroPortrait = pinfo.mainCustomHeroPortrait;
 			}
 

+ 1 - 1
test/map/CMapFormatTest.cpp

@@ -65,7 +65,7 @@ TEST(MapFormat, Random)
 	CMapGenerator gen(opt, TEST_RANDOM_SEED);
 
 	std::unique_ptr<CMap> initialMap = gen.generate();
-	initialMap->name = "Test";
+	initialMap->name.appendRawString("Test");
 	SCOPED_TRACE("MapFormat_Random generated");
 
 	CMemoryBuffer serializeBuffer;

+ 1 - 1
test/map/MapComparer.cpp

@@ -76,7 +76,7 @@ void checkEqual(const PlayerInfo & actual, const PlayerInfo & expected)
 
 	VCMI_CHECK_FIELD_EQUAL(isFactionRandom);
 	VCMI_CHECK_FIELD_EQUAL(mainCustomHeroPortrait);
-	VCMI_CHECK_FIELD_EQUAL(mainCustomHeroName);
+	VCMI_CHECK_FIELD_EQUAL(mainCustomHeroNameTextId);
 
 	VCMI_CHECK_FIELD_EQUAL(mainCustomHeroId);