浏览代码

Introduced string identifiers for H3 texts, still WIP

Ivan Savenko 2 年之前
父节点
当前提交
bdb8e0ee5c

+ 3 - 3
client/CMT.cpp

@@ -688,9 +688,9 @@ void processCommand(const std::string &message)
 			file << data.toJson();
 		};
 
-		extractVector(VLC->generaltexth->allTexts, "generalTexts");
-		extractVector(VLC->generaltexth->jktexts, "jkTexts");
-		extractVector(VLC->generaltexth->arraytxt, "arrayTexts");
+		//extractVector(VLC->generaltexth->allTexts, "generalTexts");
+		//extractVector(VLC->generaltexth->jktexts, "jkTexts");
+		//extractVector(VLC->generaltexth->arraytxt, "arrayTexts");
 
 		std::cout << "\rExtracting done :)\n";
 		std::cout << " Extracted files can be found in " << outPath << " directory\n";

+ 1 - 1
client/CPlayerInterface.cpp

@@ -1603,7 +1603,7 @@ void CPlayerInterface::playerBlocked(int reason, bool start)
 			GH.curInt = this;
 			adventureInt->selection = nullptr;
 			adventureInt->setPlayer(playerID);
-			std::string msg = CGI->generaltexth->localizedTexts["adventureMap"]["playerAttacked"].String();
+			std::string msg = CGI->generaltexth->translate("vcmi.adventureMap.playerAttacked");
 			boost::replace_first(msg, "%s", cb->getStartInfo()->playerInfos.find(playerID)->second.name);
 			std::vector<std::shared_ptr<CComponent>> cmp;
 			cmp.push_back(std::make_shared<CComponent>(CComponent::flag, playerID.getNum(), 0));

+ 2 - 2
client/CServerHandler.cpp

@@ -176,7 +176,7 @@ void CServerHandler::startLocalServerAndConnect()
 
 	th->update();
 	
-	auto errorMsg = CGI->generaltexth->localizedTexts["server"]["errors"]["existingProcess"].String();
+	auto errorMsg = CGI->generaltexth->translate("vcmi.server.errors.existingProcess");
 	try
 	{
 		CConnection testConnection(localhostAddress, getDefaultPort(), NAME, uuid);
@@ -718,7 +718,7 @@ void CServerHandler::restoreLastSession()
 		saveSession->Bool() = false;
 	};
 	
-	CInfoWindow::showYesNoDialog(VLC->generaltexth->localizedTexts["server"]["confirmReconnect"].String(), {}, loadSession, cleanUpSession);
+	CInfoWindow::showYesNoDialog(VLC->generaltexth->translate("vcmi.server.confirmReconnect"), {}, loadSession, cleanUpSession);
 }
 
 void CServerHandler::debugStartTest(std::string filename, bool save)

+ 3 - 12
client/gui/InterfaceObjectConfigurable.cpp

@@ -92,19 +92,10 @@ std::string InterfaceObjectConfigurable::readText(const JsonNode & config) const
 	
 	const std::string delimiter = "/";
 	std::string s = config.String();
+	boost::replace_all(s, "/", "." );
+
 	logGlobal->debug("Reading text from translations by key: %s", s);
-	JsonNode translated = CGI->generaltexth->localizedTexts;
-	for(size_t p = s.find(delimiter); p != std::string::npos; p = s.find(delimiter))
-	{
-		translated = translated[s.substr(0, p)];
-		s.erase(0, p + delimiter.length());
-	}
-	if(s == config.String())
-	{
-		logGlobal->warn("Reading non-translated text: %s", s);
-		return s;
-	}
-	return translated[s].String();
+	return CGI->generaltexth->translate(s);
 }
 
 Point InterfaceObjectConfigurable::readPosition(const JsonNode & config) const

+ 12 - 6
client/widgets/Buttons.cpp

@@ -22,6 +22,7 @@
 #include "../gui/CGuiHandler.h"
 #include "../windows/InfoWindows.h"
 #include "../../lib/CConfigHandler.h"
+#include "../../lib/CGeneralTextHandler.h"
 
 void CButton::update()
 {
@@ -285,9 +286,12 @@ std::pair<std::string, std::string> CButton::tooltip()
 	return std::pair<std::string, std::string>();
 }
 
-std::pair<std::string, std::string> CButton::tooltip(const JsonNode & localizedTexts)
+std::pair<std::string, std::string> CButton::tooltipLocalized(const std::string & key)
 {
-	return std::make_pair(localizedTexts["label"].String(), localizedTexts["help"].String());
+	return std::make_pair(
+		CGI->generaltexth->translate(key + ".label"),
+		CGI->generaltexth->translate(key + ".help")
+	);
 }
 
 std::pair<std::string, std::string> CButton::tooltip(const std::string & hover, const std::string & help)
@@ -457,10 +461,10 @@ int CToggleGroup::getSelected() const
 	return selectedID;
 }
 
-CVolumeSlider::CVolumeSlider(const Point & position, const std::string & defName, const int value, const std::pair<std::string, std::string> * const help)
+CVolumeSlider::CVolumeSlider(const Point & position, const std::string & defName, const int value, ETooltipMode mode)
 	: CIntObject(LCLICK | RCLICK | WHEEL),
 	value(value),
-	helpHandlers(help)
+	mode(mode)
 {
 	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
 	animImage = std::make_shared<CAnimImage>(std::make_shared<CAnimation>(defName), 0, 0, position.x, position.y),
@@ -517,8 +521,10 @@ void CVolumeSlider::clickRight(tribool down, bool previousState)
 	{
 		double px = GH.current->motion.x - pos.x;
 		int index = static_cast<int>(px / static_cast<double>(pos.w) * animImage->size());
-		std::string hoverText = helpHandlers[index].first;
-		std::string helpBox = helpHandlers[index].second;
+
+		size_t helpIndex = index + (mode == MUSIC ? 326 : 336);
+		std::string helpBox = CGI->generaltexth->translate("core.help." + std::to_string(helpIndex) + ".help" );
+
 		if(!helpBox.empty())
 			CRClickPopup::createAndPush(helpBox);
 		if(GH.statusbar)

+ 12 - 6
client/widgets/Buttons.h

@@ -115,7 +115,7 @@ public:
 
 	/// generates tooltip that can be passed into constructor
 	static std::pair<std::string, std::string> tooltip();
-	static std::pair<std::string, std::string> tooltip(const JsonNode & localizedTexts);
+	static std::pair<std::string, std::string> tooltipLocalized(const std::string & key);
 	static std::pair<std::string, std::string> tooltip(const std::string & hover, const std::string & help = "");
 };
 
@@ -191,19 +191,25 @@ public:
 /// A typical slider for volume with an animated indicator
 class CVolumeSlider : public CIntObject
 {
+public:
+	enum ETooltipMode
+	{
+		MUSIC,
+		SOUND
+	};
+
+private:
 	int value;
 	CFunctionList<void(int)> onChange;
 	std::shared_ptr<CAnimImage> animImage;
-	const std::pair<std::string, std::string> * const helpHandlers;
+	ETooltipMode mode;
 	void setVolume(const int v);
 public:
-
 	/// @param position coordinates of slider
 	/// @param defName name of def animation for slider
 	/// @param value initial value for volume
-	/// @param help pointer to first helptext of slider
-	CVolumeSlider(const Point & position, const std::string & defName, const int value,
-	              const std::pair<std::string, std::string> * const help);
+	/// @param mode that determines tooltip texts
+	CVolumeSlider(const Point & position, const std::string & defName, const int value, ETooltipMode mode);
 
 	void moveTo(int id);
 	void addCallback(std::function<void(int)> callback);

+ 1 - 1
client/widgets/CArtifactHolder.cpp

@@ -1008,7 +1008,7 @@ CCommanderArtPlace::CCommanderArtPlace(Point position, const CGHeroInstance * co
 void CCommanderArtPlace::clickLeft(tribool down, bool previousState)
 {
 	if (ourArt && text.size() && down)
-		LOCPLINT->showYesNoDialog(CGI->generaltexth->localizedTexts["commanderWindow"]["artifactMessage"].String(), [this](){ returnArtToHeroCallback(); }, [](){});
+		LOCPLINT->showYesNoDialog(CGI->generaltexth->translate("vcmi.commanderWindow.artifactMessage"), [this](){ returnArtToHeroCallback(); }, [](){});
 }
 
 void CCommanderArtPlace::clickRight(tribool down, bool previousState)

+ 3 - 3
client/windows/CAdvmapInterface.cpp

@@ -1215,7 +1215,7 @@ void CAdvMapInt::keyPressed(const SDL_KeyboardEvent & key)
 			if(itr != LOCPLINT->towns.end())
 				LOCPLINT->showThievesGuildWindow(*itr);
 			else
-				LOCPLINT->showInfoDialog(CGI->generaltexth->localizedTexts["adventureMap"]["noTownWithTavern"].String());
+				LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.adventureMap.noTownWithTavern"));
 		}
 		return;
 	case SDLK_i:
@@ -1247,7 +1247,7 @@ void CAdvMapInt::keyPressed(const SDL_KeyboardEvent & key)
 	case SDLK_r:
 		if(isActive() && LOCPLINT->ctrlPressed())
 		{
-			LOCPLINT->showYesNoDialog(CGI->generaltexth->localizedTexts["adventureMap"]["confirmRestartGame"].String(),
+			LOCPLINT->showYesNoDialog(CGI->generaltexth->translate("vcmi.adventureMap.confirmRestartGame"),
 				[](){ LOCPLINT->sendCustomEvent(EUserEvent::RESTART_GAME); }, nullptr);
 		}
 		return;
@@ -1306,7 +1306,7 @@ void CAdvMapInt::keyPressed(const SDL_KeyboardEvent & key)
 				if(townWithMarket) //if any town has marketplace, open window
 					GH.pushIntT<CMarketplaceWindow>(townWithMarket);
 				else //if not - complain
-					LOCPLINT->showInfoDialog(CGI->generaltexth->localizedTexts["adventureMap"]["noTownWithMarket"].String());
+					LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.adventureMap.noTownWithMarket"));
 			}
 			else if(isActive()) //no ctrl, advmapint is on the top => switch to town
 			{

+ 4 - 4
client/windows/CCastleInterface.cpp

@@ -851,7 +851,7 @@ void CCastleBuildings::enterToTheQuickRecruitmentWindow()
 	if(hasSomeoneToRecruit)
 		GH.pushIntT<QuickRecruitmentWindow>(town, pos);
 	else
-		CInfoWindow::showInfoDialog(CGI->generaltexth->localizedTexts["townHall"]["noCreaturesToRecruit"].String(), {});
+		CInfoWindow::showInfoDialog(CGI->generaltexth->translate("vcmi.townHall.noCreaturesToRecruit"), {});
 }
 
 void CCastleBuildings::enterFountain(const BuildingID & building, BuildingSubID::EBuildingSubID subID, BuildingID::EBuildingID upgrades)
@@ -870,8 +870,8 @@ void CCastleBuildings::enterFountain(const BuildingID & building, BuildingSubID:
 	{
 		auto buildingName = town->town->getSpecialBuilding(subID)->Name();
 
-		hasNotProduced = std::string(CGI->generaltexth->localizedTexts["townHall"]["hasNotProduced"].String());
-		hasProduced = std::string(CGI->generaltexth->localizedTexts["townHall"]["hasProduced"].String());
+		hasNotProduced = std::string(CGI->generaltexth->translate("vcmi.townHall.hasNotProduced"));
+		hasProduced = std::string(CGI->generaltexth->translate("vcmi.townHall.hasProduced"));
 		boost::algorithm::replace_first(hasNotProduced, "%s", buildingName);
 		boost::algorithm::replace_first(hasProduced, "%s", buildingName);
 	}
@@ -1468,7 +1468,7 @@ std::string CBuildWindow::getTextForState(int state)
 		}
 	case EBuildingState::MISSING_BASE:
 		{
-			std::string msg = CGI->generaltexth->localizedTexts["townHall"]["missingBase"].String();
+			std::string msg = CGI->generaltexth->translate("vcmi.townHall.missingBase");
 			ret = boost::str(boost::format(msg) % town->town->buildings.at(building->upgrade)->Name());
 			break;
 		}

+ 3 - 4
client/windows/CCreatureWindow.cpp

@@ -348,8 +348,8 @@ CStackWindow::ButtonsSection::ButtonsSection(CStackWindow * owner, int yOffset)
 				parent->redraw(); // FIXME: enable/disable don't redraw screen themselves
 			};
 
-			const JsonNode & text = VLC->generaltexth->localizedTexts["creatureWindow"][btnIDs[buttonIndex]];
-			parent->switchButtons[buttonIndex] = std::make_shared<CButton>(Point(302 + (int)buttonIndex*40, 5), "stackWindow/upgradeButton", CButton::tooltip(text), onSwitch);
+			std::string tooltipText = "vcmi.creatureWindow." + btnIDs[buttonIndex];
+			parent->switchButtons[buttonIndex] = std::make_shared<CButton>(Point(302 + (int)buttonIndex*40, 5), "stackWindow/upgradeButton", CButton::tooltipLocalized(tooltipText), onSwitch);
 			parent->switchButtons[buttonIndex]->addOverlay(std::make_shared<CAnimImage>("stackWindow/switchModeIcons", buttonIndex));
 		}
 		parent->switchButtons[parent->activeTab]->disable();
@@ -601,13 +601,12 @@ CStackWindow::MainSection::MainSection(CStackWindow * owner, int yOffset, bool s
 			parent->stackArtifactIcon = std::make_shared<CAnimImage>("ARTIFACT", art->artType->iconIndex, 0, pos.x, pos.y);
 			parent->stackArtifactHelp = std::make_shared<LRClickableAreaWTextComp>(Rect(pos, Point(44, 44)), CComponent::artifact);
 			parent->stackArtifactHelp->type = art->artType->id;
-			const JsonNode & text =	VLC->generaltexth->localizedTexts["creatureWindow"]["returnArtifact"];
 
 			if(parent->info->owner)
 			{
 				parent->stackArtifactButton = std::make_shared<CButton>(
 						Point(pos.x - 2 , pos.y + 46), "stackWindow/cancelButton",
-						CButton::tooltip(text),	[=]()
+						CButton::tooltipLocalized("vcmi.creatureWindow.returnArtifact"),	[=]()
 				{
 					parent->removeStackArtifact(ArtifactPosition::CREATURE_SLOT);
 				});

+ 1 - 2
client/windows/CHeroWindow.cpp

@@ -128,8 +128,7 @@ CHeroWindow::CHeroWindow(const CGHeroInstance * hero)
 
 	if(hero->commander)
 	{
-		auto texts = CGI->generaltexth->localizedTexts["heroWindow"]["openCommander"];
-		commanderButton = std::make_shared<CButton>(Point(317, 18), "buttons/commander", CButton::tooltip(texts), [&](){ commanderWindow(); }, SDLK_c);
+		commanderButton = std::make_shared<CButton>(Point(317, 18), "buttons/commander", CButton::tooltipLocalized("vcmi.heroWindow.openCommander"), [&](){ commanderWindow(); }, SDLK_c);
 	}
 
 	//right list of heroes

+ 1 - 1
client/windows/CKingdomInterface.cpp

@@ -594,7 +594,7 @@ void CKingdomInterface::generateMinesList(const std::vector<const CGObjectInstan
 	for(int i=0; i<7; i++)
 	{
 		std::string value = boost::lexical_cast<std::string>(minesCount[i]);
-		auto data = std::make_shared<InfoBoxCustom>(value, "", "OVMINES", i, CGI->generaltexth->mines[i].first);
+		auto data = std::make_shared<InfoBoxCustom>(value, "", "OVMINES", i, CGI->generaltexth->translate("core.minename", i));
 		minesBox[i] = std::make_shared<InfoBox>(Point(20+i*80, 31+footerPos), InfoBox::POS_INSIDE, InfoBox::SIZE_SMALL, data);
 		minesBox[i]->removeUsedEvents(LCLICK|RCLICK); //fixes #890 - mines boxes ignore clicks
 	}

+ 2 - 4
client/windows/CQuestLog.cpp

@@ -126,15 +126,13 @@ CQuestLog::CQuestLog (const std::vector<QuestInfo> & Quests)
 {
 	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
 
-	const JsonNode & texts = CGI->generaltexth->localizedTexts["questLog"];
-
 	minimap = std::make_shared<CQuestMinimap>(Rect(12, 12, 169, 169));
 	// TextBox have it's own 4 pixel padding from top at least for English. To achieve 10px from both left and top only add 6px margin
 	description = std::make_shared<CTextBox>("", Rect(205, 18, 385, DESCRIPTION_HEIGHT_MAX), CSlider::BROWN, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE);
 	ok = std::make_shared<CButton>(Point(539, 398), "IOKAY.DEF", CGI->generaltexth->zelp[445], std::bind(&CQuestLog::close, this), SDLK_RETURN);
 	// Both button and lable are shifted to -2px by x and y to not make them actually look like they're on same line with quests list and ok button
-	hideCompleteButton = std::make_shared<CToggleButton>(Point(10, 396), "sysopchk.def", CButton::tooltip(texts["hideComplete"]), std::bind(&CQuestLog::toggleComplete, this, _1));
-	hideCompleteLabel = std::make_shared<CLabel>(46, 398, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, texts["hideComplete"]["label"].String());
+	hideCompleteButton = std::make_shared<CToggleButton>(Point(10, 396), "sysopchk.def", CButton::tooltipLocalized("vcmi.questLog.hideComplete"), std::bind(&CQuestLog::toggleComplete, this, _1));
+	hideCompleteLabel = std::make_shared<CLabel>(46, 398, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->translate("vcmi.questLog.hideComplete.label"));
 	slider = std::make_shared<CSlider>(Point(166, 195), 191, std::bind(&CQuestLog::sliderMoved, this, _1), QUEST_COUNT, 0, false, CSlider::BROWN);
 
 	recreateLabelList();

+ 2 - 2
client/windows/CSpellWindow.cpp

@@ -562,7 +562,7 @@ void CSpellWindow::SpellArea::clickLeft(tribool down, bool previousState)
 				if(!texts.empty())
 					owner->myInt->showInfoDialog(texts.front());
 				else
-					owner->myInt->showInfoDialog(CGI->generaltexth->localizedTexts["adventureMap"]["spellUnknownProblem"].String());
+					owner->myInt->showInfoDialog(CGI->generaltexth->translate("vcmi.adventureMap.spellUnknownProblem"));
 			}
 		}
 		else //adventure spell
@@ -658,7 +658,7 @@ void CSpellWindow::SpellArea::setSpell(const CSpell * spell)
 		{
 			boost::format fmt("%s/%s");
 			fmt % CGI->generaltexth->allTexts[171 + mySpell->level];
-			fmt % CGI->generaltexth->levels.at(3+(schoolLevel-1));//lines 4-6
+			fmt % CGI->generaltexth->levels[3+(schoolLevel-1)];//lines 4-6
 			level->setText(fmt.str());
 		}
 		else

+ 13 - 12
client/windows/GUIClasses.cpp

@@ -449,14 +449,12 @@ CSystemOptionsWindow::CSystemOptionsWindow()
 	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
 	title = std::make_shared<CLabel>(242, 32, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[568]);
 
-	const JsonNode & texts = CGI->generaltexth->localizedTexts["systemOptions"];
-
 	//left window section
 	leftGroup = std::make_shared<CLabelGroup>(FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW);
 	leftGroup->add(122,  64, CGI->generaltexth->allTexts[569]);
 	leftGroup->add(122, 130, CGI->generaltexth->allTexts[570]);
 	leftGroup->add(122, 196, CGI->generaltexth->allTexts[571]);
-	leftGroup->add(122, 262, texts["resolutionButton"]["label"].String());
+	leftGroup->add(122, 262, CGI->generaltexth->translate("vcmi.systemOptions.resolutionButton.label"));
 	leftGroup->add(122, 347, CGI->generaltexth->allTexts[394]);
 	leftGroup->add(122, 412, CGI->generaltexth->allTexts[395]);
 
@@ -466,8 +464,8 @@ CSystemOptionsWindow::CSystemOptionsWindow()
 	rightGroup->add(282, 89,  CGI->generaltexth->allTexts[573]);
 	rightGroup->add(282, 121, CGI->generaltexth->allTexts[574]);
 	rightGroup->add(282, 153, CGI->generaltexth->allTexts[577]);
-	rightGroup->add(282, 185, texts["creatureWindowButton"]["label"].String());
-	rightGroup->add(282, 217, texts["fullscreenButton"]["label"].String());
+	//rightGroup->add(282, 185, CGI->generaltexth->translate("vcmi.systemOptions.creatureWindowButton.label"));
+	rightGroup->add(282, 217, CGI->generaltexth->translate("vcmi.systemOptions.fullscreenButton.label"));
 
 	//setting up buttons
 	load = std::make_shared<CButton>(Point(246,  298), "SOLOAD.DEF", CGI->generaltexth->zelp[321], [&](){ bloadf(); }, SDLK_l);
@@ -520,10 +518,10 @@ CSystemOptionsWindow::CSystemOptionsWindow()
 	mapScrollSpeed->setSelected((int)settings["adventure"]["scrollSpeed"].Float());
 	mapScrollSpeed->addCallback(std::bind(&setIntSetting, "adventure", "scrollSpeed", _1));
 
-	musicVolume = std::make_shared<CVolumeSlider>(Point(29, 359), "syslb.def", CCS->musich->getVolume(), &CGI->generaltexth->zelp[326]);
+	musicVolume = std::make_shared<CVolumeSlider>(Point(29, 359), "syslb.def", CCS->musich->getVolume(), CVolumeSlider::MUSIC);
 	musicVolume->addCallback(std::bind(&setIntSetting, "general", "music", _1));
 
-	effectsVolume = std::make_shared<CVolumeSlider>(Point(29, 425), "syslb.def", CCS->soundh->getVolume(), &CGI->generaltexth->zelp[336]);
+	effectsVolume = std::make_shared<CVolumeSlider>(Point(29, 425), "syslb.def", CCS->soundh->getVolume(), CVolumeSlider::SOUND);
 	effectsVolume->addCallback(std::bind(&setIntSetting, "general", "sound", _1));
 
 	showReminder = std::make_shared<CToggleButton>(Point(246, 87), "sysopchk.def", CGI->generaltexth->zelp[361], [&](bool value)
@@ -544,7 +542,7 @@ CSystemOptionsWindow::CSystemOptionsWindow()
 	});
 	spellbookAnim->setSelected(settings["video"]["spellbookAnimation"].Bool());
 
-	fullscreen = std::make_shared<CToggleButton>(Point(246, 215), "sysopchk.def", CButton::tooltip(texts["fullscreenButton"]), [&](bool value)
+	fullscreen = std::make_shared<CToggleButton>(Point(246, 215), "sysopchk.def", CButton::tooltipLocalized("vcmi.systemOptions.fullscreenButton"), [&](bool value)
 	{
 		setBoolSetting("video", "fullscreen", value);
 	});
@@ -552,7 +550,7 @@ CSystemOptionsWindow::CSystemOptionsWindow()
 
 	onFullscreenChanged([&](const JsonNode &newState){ fullscreen->setSelected(newState.Bool());});
 
-	gameResButton = std::make_shared<CButton>(Point(28, 275),"buttons/resolution", CButton::tooltip(texts["resolutionButton"]), std::bind(&CSystemOptionsWindow::selectGameRes, this), SDLK_g);
+	gameResButton = std::make_shared<CButton>(Point(28, 275),"buttons/resolution", CButton::tooltipLocalized("vcmi.systemOptions.resolutionButton"), std::bind(&CSystemOptionsWindow::selectGameRes, this), SDLK_g);
 
 	const auto & screenRes = settings["video"]["screenRes"];
 	gameResLabel = std::make_shared<CLabel>(170, 292, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, resolutionToString(screenRes["width"].Integer(), screenRes["height"].Integer()));
@@ -561,7 +559,6 @@ CSystemOptionsWindow::CSystemOptionsWindow()
 void CSystemOptionsWindow::selectGameRes()
 {
 	std::vector<std::string> items;
-	const JsonNode & texts = CGI->generaltexth->localizedTexts["systemOptions"]["resolutionMenu"];
 
 #ifndef VCMI_IOS
 	SDL_Rect displayBounds;
@@ -585,7 +582,11 @@ void CSystemOptionsWindow::selectGameRes()
 		++i;
 	}
 
-	GH.pushIntT<CObjectListWindow>(items, nullptr, texts["label"].String(), texts["help"].String(), std::bind(&CSystemOptionsWindow::setGameRes, this, _1), currentResolutionIndex);
+	GH.pushIntT<CObjectListWindow>(items, nullptr,
+								   CGI->generaltexth->translate("vcmi.systemOptions.resolutionMenu.label"),
+								   CGI->generaltexth->translate("vcmi.systemOptions.resolutionMenu.help"),
+								   std::bind(&CSystemOptionsWindow::setGameRes, this, _1),
+								   currentResolutionIndex);
 }
 
 void CSystemOptionsWindow::setGameRes(int index)
@@ -1217,7 +1218,7 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2,
 	garr->addSplitBtn(std::make_shared<CButton>( Point( 10, qeLayout ? 122 : 132), "TSBTNS.DEF", CButton::tooltip(CGI->generaltexth->tcommands[3]), splitButtonCallback));
 	garr->addSplitBtn(std::make_shared<CButton>( Point(744, qeLayout ? 122 : 132), "TSBTNS.DEF", CButton::tooltip(CGI->generaltexth->tcommands[3]), splitButtonCallback));
 
-	if (qeLayout && (CGI->generaltexth->qeModCommands.size() >= 5))
+	if (qeLayout )
 	{
 		moveAllGarrButtonLeft    = std::make_shared<CButton>(Point(325, 118), QUICK_EXCHANGE_MOD_PREFIX + "/armRight.DEF", CButton::tooltip(CGI->generaltexth->qeModCommands[1]), controller.onMoveArmyToRight());
 		echangeGarrButton        = std::make_shared<CButton>(Point(377, 118), QUICK_EXCHANGE_MOD_PREFIX + "/swapAll.DEF", CButton::tooltip(CGI->generaltexth->qeModCommands[2]), controller.onSwapArmy());

+ 68 - 125
config/translate.json

@@ -1,127 +1,70 @@
-// This file contains all translateable strings added in VCMI
 {
-	"adventureMap":
-	{
-		"monsterThreat" :
-		{
-			"title" : "\n\n Threat: ",
-			"levels" :
-			[
-				"Effortless",
-				"Very Weak",
-				"Weak",
-				"A bit weaker",
-				"Equal",
-				"A bit stronger",
-				"Strong",
-				"Very Strong",
-				"Challenging",
-				"Overpowering",
-				"Deadly",
-				"Impossible"
-			]
-		},
-		"confirmRestartGame" : "Are you sure you want to restart game?",
-		"noTownWithMarket": "No available marketplace!",
-		"noTownWithTavern": "No available town with tavern!",
-		"spellUnknownProblem": "Unknown problem with this spell, no more information available.",
-		"playerAttacked" : "Player has been attacked: %s"
-	},
-	"server" :
-	{
-		"errors" :
-		{
-			"existingProcess" : "Another vcmiserver process is running, please terminate it first",
-			"modsIncompatibility" : "Required mods to load game:"
-		},
-		"confirmReconnect" : "Connect to the last session?"
-	},
-	"systemOptions" :
-	{
-		"fullscreenButton" :
-		{
-			"label" : "Fullscreen",
-			"help"  : "{Fullscreen}\n\n If selected, VCMI will run in fullscreen mode, othervice VCMI will run in window"
-		},
-		"resolutionButton" :
-		{
-			"label" : "Resolution",
-			"help"  : "{Select resolution}\n\n Change in-game screen resolution. Game restart required to apply new resolution."
-		},
-		"resolutionMenu" :
-		{
-			"label" : "Select resolution",
-			"help" : "Change in-game screen resolution."
-		}
-	},
-	"townHall" :
-	{
-		"missingBase" : "Base building %s must be built first",
-		"noCreaturesToRecruit" : "There are no creatures to recruit!",
-		"greetingManaVortex" : "As you near the %s your body is filled with new energy. You have doubled your normal spell points.",
-		"greetingKnowledge" : "You study the glyphs on the %s and gain insight into the workings of various magics (+1 Knowledge).",
-		"greetingSpellPower" : "The %s teaches you new ways to focus your magical powers (+1 Power).",
-		"greetingExperience" : "A visit to the %s teaches you many new skills (+1000 Experience).",
-		"greetingAttack" : "Some time spent at the %s allows you to learn more effective combat skills (+1 Attack Skill).",
-		"greetingDefence" : "Spending time in the %s, the experienced warriors therein teach you additional defensive skills (+1 Defense).",
-		"hasNotProduced" : "The %s has not produced anything yet.",
-		"hasProduced" : "The %s produced %d %s this week.",
-		"greetingCustomBonus" : "%s gives you +%d %s%s",
-		"greetingCustomUntil" : " until next battle.",
-		"greetingInTownMagicWell" : "%s has restored your spell points to maximum."
-	},
-	"logicalExpressions" :
-	{
-		"anyOf"  :  "Any of the following:",
-		"allOf"  :  "All of the following:",
-		"noneOf" : "None of the following:"
-	},
-	"heroWindow" :
-	{
-		"openCommander" :
-		{
-			"label" : "Open commander window",
-			"help" : "Displays information about commander of this hero"
-		}
-	},
-	"commanderWindow":
-	{
-		"artifactMessage": "Do you want to give this artifact back to hero?"
-	},
-	"creatureWindow" :
-	{
-		"showBonuses" :
-		{
-			"label" : "Switch to bonuses view",
-			"help" : "Displays all active bonuses of the commander"
-		},
-		"showSkills" :
-		{
-			"label" : "Switch to skills view",
-			"help" : "Displays all learned skills of the commander"
-		},
-		"returnArtifact" :
-		{
-			"label" : "Give back artifact",
-			"help" : "Use this button to return stack artifact back into hero backpack"
-		}
-	},
-	"questLog" :
-	{
-		"hideComplete" :
-		{
-			"label" : "Hide complete quests",
-			"help" : "Hide all quests that already completed"
-		}
-	},
-	"randomMapTab":
-	{
-		"widgets":
-		{
-			"defaultTemplate": "default",
-			"templateLabel": "Template",
-			"teamAlignmentsButton": "Setup...",
-			"teamAlignmentsLabel": "Team alignments"
-		}
-	}
+	"vcmi.adventureMap.monsterThreat.title"     : "\n\n Threat: ",
+	"vcmi.adventureMap.monsterThreat.levels.0"  : "Effortless",
+	"vcmi.adventureMap.monsterThreat.levels.1"  : "Very Weak",
+	"vcmi.adventureMap.monsterThreat.levels.2"  : "Weak",
+	"vcmi.adventureMap.monsterThreat.levels.3"  : "A bit weaker",
+	"vcmi.adventureMap.monsterThreat.levels.4"  : "Equal",
+	"vcmi.adventureMap.monsterThreat.levels.5"  : "A bit stronger",
+	"vcmi.adventureMap.monsterThreat.levels.6"  : "Strong",
+	"vcmi.adventureMap.monsterThreat.levels.7"  : "Very Strong",
+	"vcmi.adventureMap.monsterThreat.levels.8"  : "Challenging",
+	"vcmi.adventureMap.monsterThreat.levels.9"  : "Overpowering",
+	"vcmi.adventureMap.monsterThreat.levels.10" : "Deadly",
+	"vcmi.adventureMap.monsterThreat.levels.11" : "Impossible",
+
+	"vcmi.adventureMap.confirmRestartGame"  : "Are you sure you want to restart game?",
+	"vcmi.adventureMap.noTownWithMarket"    : "No available marketplace!",
+	"vcmi.adventureMap.noTownWithTavern"    : "No available town with tavern!",
+	"vcmi.adventureMap.spellUnknownProblem" : "Unknown problem with this spell, no more information available.",
+	"vcmi.adventureMap.playerAttacked"      : "Player has been attacked: %s",
+
+	"vcmi.server.errors.existingProcess"     : "Another vcmiserver process is running, please terminate it first",
+	"vcmi.server.errors.modsIncompatibility" : "Required mods to load game:",
+	"vcmi.server.confirmReconnect"          : "Connect to the last session?",
+
+	"vcmi.systemOptions.fullscreenButton.label" : "Fullscreen",
+	"vcmi.systemOptions.fullscreenButton.help"  : "{Fullscreen}\n\n If selected, VCMI will run in fullscreen mode, othervice VCMI will run in window",
+	"vcmi.systemOptions.resolutionButton.label" : "Resolution",
+	"vcmi.systemOptions.resolutionButton.help"  : "{Select resolution}\n\n Change in-game screen resolution. Game restart required to apply new resolution.",
+	"vcmi.systemOptions.resolutionMenu.label"   : "Select resolution",
+	"vcmi.systemOptions.resolutionMenu.help"    : "Change in-game screen resolution.",
+
+	"vcmi.townHall.missingBase"             : "Base building %s must be built first",
+	"vcmi.townHall.noCreaturesToRecruit"    : "There are no creatures to recruit!",
+	"vcmi.townHall.greetingManaVortex"      : "As you near the %s your body is filled with new energy. You have doubled your normal spell points.",
+	"vcmi.townHall.greetingKnowledge"       : "You study the glyphs on the %s and gain insight into the workings of various magics (+1 Knowledge).",
+	"vcmi.townHall.greetingSpellPower"      : "The %s teaches you new ways to focus your magical powers (+1 Power).",
+	"vcmi.townHall.greetingExperience"      : "A visit to the %s teaches you many new skills (+1000 Experience).",
+	"vcmi.townHall.greetingAttack"          : "Some time spent at the %s allows you to learn more effective combat skills (+1 Attack Skill).",
+	"vcmi.townHall.greetingDefence"         : "Spending time in the %s, the experienced warriors therein teach you additional defensive skills (+1 Defense).",
+	"vcmi.townHall.hasNotProduced"          : "The %s has not produced anything yet.",
+	"vcmi.townHall.hasProduced"             : "The %s produced %d %s this week.",
+	"vcmi.townHall.greetingCustomBonus"     : "%s gives you +%d %s%s",
+	"vcmi.townHall.greetingCustomUntil"     : " until next battle.",
+	"vcmi.townHall.greetingInTownMagicWell" : "%s has restored your spell points to maximum.",
+
+	"vcmi.logicalExpressions.anyOf"  : "Any of the following:",
+	"vcmi.logicalExpressions.allOf"  : "All of the following:",
+	"vcmi.logicalExpressions.noneOf" : "None of the following:",
+
+	"vcmi.heroWindow.openCommander.label" : "Open commander window",
+	"vcmi.heroWindow.openCommander.help"  : "Displays information about commander of this hero",
+
+	"vcmi.commanderWindow.artifactMessage" : "Do you want to give this artifact back to hero?",
+
+	"vcmi.creatureWindow.showBonuses.label"    : "Switch to bonuses view",
+	"vcmi.creatureWindow.showBonuses.help"     : "Displays all active bonuses of the commander",
+	"vcmi.creatureWindow.showSkills.label"     : "Switch to skills view",
+	"vcmi.creatureWindow.showSkills.help"      : "Displays all learned skills of the commander",
+	"vcmi.creatureWindow.returnArtifact.label" : "Give back artifact",
+	"vcmi.creatureWindow.returnArtifact.help"  : "Use this button to return stack artifact back into hero backpack",
+
+	"vcmi.questLog.hideComplete.label" : "Hide complete quests",
+	"vcmi.questLog.hideComplete.help"  : "Hide all quests that already completed",
+
+	"vcmi.randomMapTab.widgets.defaultTemplate"      : "default",
+	"vcmi.randomMapTab.widgets.templateLabel"        : "Template",
+	"vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Setup...",
+	"vcmi.randomMapTab.widgets.teamAlignmentsLabel"  : "Team alignments"
 }

+ 14 - 23
lib/CGameState.cpp

@@ -112,11 +112,11 @@ void MetaString::getLocalString(const std::pair<ui8,ui32> &txt, std::string &dst
 	}
 	else if(type == MINE_NAMES)
 	{
-		dst = VLC->generaltexth->mines[ser].first;
+		dst = VLC->generaltexth->translate("core.minename", ser);
 	}
 	else if(type == MINE_EVNTS)
 	{
-		dst = VLC->generaltexth->mines[ser].second;
+		dst = VLC->generaltexth->translate("core.mineevnt", ser);
 	}
 	else if(type == SPELL_NAME)
 	{
@@ -136,48 +136,40 @@ void MetaString::getLocalString(const std::pair<ui8,ui32> &txt, std::string &dst
 	}
 	else
 	{
-		std::vector<std::string> *vec;
 		switch(type)
 		{
 		case GENERAL_TXT:
-			vec = &VLC->generaltexth->allTexts;
+			dst = VLC->generaltexth->translate("core.allTexts", ser);
 			break;
 		case XTRAINFO_TXT:
-			vec = &VLC->generaltexth->xtrainfo;
+			dst = VLC->generaltexth->translate("core.xtrainfo", ser);
 			break;
 		case RES_NAMES:
-			vec = &VLC->generaltexth->restypes;
+			dst = VLC->generaltexth->translate("core.restypes", ser);
 			break;
 		case ARRAY_TXT:
-			vec = &VLC->generaltexth->arraytxt;
+			dst = VLC->generaltexth->translate("core.arraytxt", ser);
 			break;
 		case CREGENS:
-			vec = &VLC->generaltexth->creGens;
+			dst = VLC->generaltexth->translate("core.crgen1", ser);
 			break;
 		case CREGENS4:
-			vec = &VLC->generaltexth->creGens4;
+			dst = VLC->generaltexth->translate("core.crgen4", ser);
 			break;
 		case ADVOB_TXT:
-			vec = &VLC->generaltexth->advobtxt;
+			dst = VLC->generaltexth->translate("core.advevent", ser);
 			break;
 		case COLOR:
-			vec = &VLC->generaltexth->capColors;
+			dst = VLC->generaltexth->translate("vcmi.capitalColors", ser);
 			break;
 		case JK_TXT:
-			vec = &VLC->generaltexth->jktexts;
+			dst = VLC->generaltexth->translate("core.jktext", ser);
 			break;
 		default:
 			logGlobal->error("Failed string substitution because type is %d", type);
 			dst = "#@#";
 			return;
 		}
-		if(vec->size() <= ser)
-		{
-			logGlobal->error("Failed string substitution with type %d because index %d is out of bounds!", type, ser);
-			dst = "#!#";
-		}
-		else
-			dst = (*vec)[ser];
 	}
 }
 
@@ -2182,11 +2174,10 @@ void CGameState::updateRumor()
 			FALLTHROUGH
 
 		case RumorState::TYPE_RAND:
+			auto vector = VLC->generaltexth->findStringsWithPrefix("core.randtvrn");
 			do
-			{
-				rumorId = rand.nextInt((int)VLC->generaltexth->tavernRumors.size() - 1);
-			}
-			while(!VLC->generaltexth->tavernRumors[rumorId].length());
+				rumorId = rand.nextInt((int)vector.size() - 1);
+			while(vector[rumorId].empty());
 
 			break;
 		}

+ 153 - 55
lib/CGeneralTextHandler.cpp

@@ -300,58 +300,118 @@ bool CLegacyConfigParser::endLine()
 	return curr < end;
 }
 
-void CGeneralTextHandler::readToVector(std::string sourceName, std::vector<std::string> &dest)
+void CGeneralTextHandler::readToVector(std::string sourceID, std::string sourceName)
 {
 	CLegacyConfigParser parser(sourceName);
+	size_t index = 0;
 	do
 	{
-		dest.push_back(parser.readString());
+		registerH3String(sourceID, index, parser.readString());
+		index += 1;
 	}
 	while (parser.endLine());
 }
 
-CGeneralTextHandler::CGeneralTextHandler()
-{
-	std::vector<std::string> h3mTerrainNames;
-	readToVector("DATA/VCDESC.TXT",   victoryConditions);
-	readToVector("DATA/LCDESC.TXT",   lossCondtions);
-	readToVector("DATA/TCOMMAND.TXT", tcommands);
-	readToVector("DATA/HALLINFO.TXT", hcommands);
-	readToVector("DATA/CASTINFO.TXT", fcommands);
-	readToVector("DATA/ADVEVENT.TXT", advobtxt);
-	readToVector("DATA/XTRAINFO.TXT", xtrainfo);
-	readToVector("DATA/RESTYPES.TXT", restypes);
-	readToVector("DATA/TERRNAME.TXT", h3mTerrainNames);
-	readToVector("DATA/RANDSIGN.TXT", randsign);
-	readToVector("DATA/CRGEN1.TXT",   creGens);
-	readToVector("DATA/CRGEN4.TXT",   creGens4);
-	readToVector("DATA/OVERVIEW.TXT", overview);
-	readToVector("DATA/ARRAYTXT.TXT", arraytxt);
-	readToVector("DATA/PRISKILL.TXT", primarySkillNames);
-	readToVector("DATA/JKTEXT.TXT",   jktexts);
-	readToVector("DATA/TVRNINFO.TXT", tavernInfo);
-	readToVector("DATA/RANDTVRN.TXT", tavernRumors);
-	readToVector("DATA/TURNDUR.TXT",  turnDurations);
-	readToVector("DATA/HEROSCRN.TXT", heroscrn);
-	readToVector("DATA/TENTCOLR.TXT", tentColors);
-	readToVector("DATA/SKILLLEV.TXT", levels);
-	
-	for(int i = 0; i < h3mTerrainNames.size(); ++i)
-	{
-		terrainNames[i] = h3mTerrainNames[i];
-	}
-	for(const auto & terrain : VLC->terrainTypeHandler->terrains())
-	{
-		if(!terrain.terrainText.empty())
-			terrainNames[terrain.id] = terrain.terrainText;
-	}
-	
+const std::string & CGeneralTextHandler::translate(const std::string & identifier, size_t index) const
+{
+	return translate(identifier + std::to_string(index));
+}
+
+const std::string & CGeneralTextHandler::translate(const std::string & identifier) const
+{
+	return deserialize(identifier);
+}
+
+const std::string & CGeneralTextHandler::serialize(const std::string & identifier) const
+{
+	assert(stringsIdentifiers.count(identifier));
+	return stringsIdentifiers.at(identifier);
+}
+
+const std::string & CGeneralTextHandler::deserialize(const std::string & identifier) const
+{
+	static const std::string emptyString;
+
+	if (stringsLocalizations.count(identifier))
+		return stringsLocalizations.at(identifier);
+	logGlobal->error("Unable to find localization for string '%s'", identifier);
+	return emptyString;
+}
+
+void CGeneralTextHandler::registerH3String(const std::string & file, size_t index, const std::string & localized)
+{
+	registerString(file + '.' + std::to_string(index), localized);
+}
+
+void CGeneralTextHandler::registerString(const std::string & UID, const std::string & localized)
+{
+	stringsIdentifiers[localized] = UID;
+	stringsLocalizations[UID] = localized;
+}
+
+CGeneralTextHandler::CGeneralTextHandler():
+	victoryConditions(*this, "core.vcdesc"   ),
+	lossCondtions    (*this, "core.lcdesc"   ),
+	colors           (*this, "core.plcolors" ),
+	tcommands        (*this, "core.tcommand" ),
+	hcommands        (*this, "core.hallinfo" ),
+	fcommands        (*this, "core.castinfo" ),
+	advobtxt         (*this, "core.advevent" ),
+	xtrainfo         (*this, "core.xtrainfo" ),
+	restypes         (*this, "core.restypes" ),
+	terrainNames     (*this, "core.terrname" ),
+	randsign         (*this, "core.randsign" ),
+	creGens          (*this, "core.crgen1"   ),
+	creGens4         (*this, "core.crgen4"   ),
+	overview         (*this, "core.overview" ),
+	arraytxt         (*this, "core.arraytxt" ),
+	primarySkillNames(*this, "core.priskill" ),
+	jktexts          (*this, "core.jktext"   ),
+	tavernInfo       (*this, "core.tvrninfo" ),
+	tavernRumors     (*this, "core.randtvrn" ),
+	turnDurations    (*this, "core.turndur"  ),
+	heroscrn         (*this, "core.heroscrn" ),
+	tentColors       (*this, "core.tentcolr" ),
+	levels           (*this, "core.skilllev" ),
+	zelp             (*this, "core.help"     ),
+	// pseudo-array, that don't have H3 file with same name
+	capColors        (*this, "vcmi.capitalColors"  ),
+	qeModCommands    (*this, "vcmi.quickExchange" )
+{
+	readToVector("core.vcdesc",   "DATA/VCDESC.TXT"   );
+	readToVector("core.lcdesc",   "DATA/LCDESC.TXT"   );
+	readToVector("core.tcommand", "DATA/TCOMMAND.TXT" );
+	readToVector("core.hallinfo", "DATA/HALLINFO.TXT" );
+	readToVector("core.castinfo", "DATA/CASTINFO.TXT" );
+	readToVector("core.advevent", "DATA/ADVEVENT.TXT" );
+	readToVector("core.xtrainfo", "DATA/XTRAINFO.TXT" );
+	readToVector("core.restypes", "DATA/RESTYPES.TXT" );
+	readToVector("core.terrname", "DATA/TERRNAME.TXT" );
+	readToVector("core.randsign", "DATA/RANDSIGN.TXT" );
+	readToVector("core.crgen1",   "DATA/CRGEN1.TXT"   );
+	readToVector("core.crgen4",   "DATA/CRGEN4.TXT"   );
+	readToVector("core.overview", "DATA/OVERVIEW.TXT" );
+	readToVector("core.arraytxt", "DATA/ARRAYTXT.TXT" );
+	readToVector("core.priskill", "DATA/PRISKILL.TXT" );
+	readToVector("core.jktext",   "DATA/JKTEXT.TXT"   );
+	readToVector("core.tvrninfo", "DATA/TVRNINFO.TXT" );
+	readToVector("core.randtvrn", "DATA/RANDTVRN.TXT" );
+	readToVector("core.turndur",  "DATA/TURNDUR.TXT"  );
+	readToVector("core.heroscrn", "DATA/HEROSCRN.TXT" );
+	readToVector("core.tentcolr", "DATA/TENTCOLR.TXT" );
+	readToVector("core.skilllev", "DATA/SKILLLEV.TXT" );
+	readToVector("core.cmpmusic", "DATA/CMPMUSIC.TXT" );
+	readToVector("core.minename", "DATA/MINENAME.TXT" );
+	readToVector("core.mineevnt", "DATA/MINEEVNT.TXT" );
 
 	static const char * QE_MOD_COMMANDS = "DATA/QECOMMANDS.TXT";
 	if (CResourceHandler::get()->existsResource(ResourceID(QE_MOD_COMMANDS, EResType::TEXT)))
-		readToVector(QE_MOD_COMMANDS, qeModCommands);
+		readToVector("vcmi.quickExchange", QE_MOD_COMMANDS);
+
+	auto vcmiTexts = JsonNode(ResourceID("config/translate.json", EResType::TEXT));
 
-	localizedTexts = JsonNode(ResourceID("config/translate.json", EResType::TEXT));
+	for ( auto const & node : vcmiTexts.Struct())
+		registerString(node.first, node.second.String());
 
 	{
 		CLegacyConfigParser parser("DATA/GENRLTXT.TXT");
@@ -359,40 +419,34 @@ CGeneralTextHandler::CGeneralTextHandler()
 		do
 		{
 			allTexts.push_back(parser.readString());
+			registerH3String("core.genrltxt", allTexts.size(), allTexts.back());
 		}
 		while (parser.endLine());
 	}
 	{
 		CLegacyConfigParser parser("DATA/HELP.TXT");
+		size_t index = 0;
 		do
 		{
 			std::string first = parser.readString();
 			std::string second = parser.readString();
-			zelp.push_back(std::make_pair(first, second));
+			registerString("core.help." + std::to_string(index) + ".label", first);
+			registerString("core.help." + std::to_string(index) + ".help",  second);
+			index += 1;
 		}
 		while (parser.endLine());
 	}
-	{
-		CLegacyConfigParser nameParser("DATA/MINENAME.TXT");
-		CLegacyConfigParser eventParser("DATA/MINEEVNT.TXT");
-
-		do
-		{
-			std::string name  = nameParser.readString();
-			std::string event = eventParser.readString();
-			mines.push_back(std::make_pair(name, event));
-		}
-		while (nameParser.endLine() && eventParser.endLine());
-	}
 	{
 		CLegacyConfigParser parser("DATA/PLCOLORS.TXT");
+		size_t index = 0;
 		do
 		{
 			std::string color = parser.readString();
-			colors.push_back(color);
 
+			registerH3String("core.plcolors", index, color);
 			color[0] = toupper(color[0]);
-			capColors.push_back(color);
+			registerH3String("vcmi.capitalColors", index, color);
+			index += 1;
 		}
 		while (parser.endLine());
 	}
@@ -403,7 +457,10 @@ CGeneralTextHandler::CGeneralTextHandler()
 		parser.endLine();
 
 		for (int i = 0; i < 6; ++i)
+		{
 			seerEmpty.push_back(parser.readString());
+			registerH3String("core.seerhut.empty", seerEmpty.size(), seerEmpty.back());
+		}
 		parser.endLine();
 
 		quests.resize(10);
@@ -414,8 +471,12 @@ CGeneralTextHandler::CGeneralTextHandler()
 			{
 				parser.readString(); //front description
 				for (int k = 0; k < 6; ++k)
+				{
 					quests[i][j].push_back(parser.readString());
 
+					registerH3String("core.seerhut.quest." + std::to_string(i) + "." + std::to_string(j), k, quests[i][j].back());
+				}
+
 				parser.endLine();
 			}
 		}
@@ -522,4 +583,41 @@ int32_t CGeneralTextHandler::pluralText(const int32_t textIndex, const int32_t c
 		return textIndex + 1;
 }
 
+std::vector<std::string> CGeneralTextHandler::findStringsWithPrefix(std::string const & prefix)
+{
+	std::vector<std::string> result;
+
+	for (auto const & entry : stringsLocalizations)
+	{
+		if (boost::algorithm::starts_with(entry.first, prefix))
+			result.push_back(entry.first);
+	}
+
+	return result;
+}
+
+LegacyTextContainer::LegacyTextContainer(CGeneralTextHandler & owner, std::string const & basePath):
+	owner(owner),
+	basePath(basePath)
+{}
+
+const std::string & LegacyTextContainer::operator[](size_t index) const
+{
+	return owner.translate(basePath + "." + std::to_string(index));
+}
+
+LegacyHelpContainer::LegacyHelpContainer(CGeneralTextHandler & owner, std::string const & basePath):
+	owner(owner),
+	basePath(basePath)
+{}
+
+std::pair<std::string, std::string> LegacyHelpContainer::operator[](size_t index) const
+{
+	return {
+		owner.translate(basePath + "." + std::to_string(index) + ".label"),
+		owner.translate(basePath + "." + std::to_string(index) + ".help")
+	};
+}
+
+
 VCMI_LIB_NAMESPACE_END

+ 72 - 28
lib/CGeneralTextHandler.h

@@ -91,50 +91,94 @@ public:
 	CLegacyConfigParser(const std::unique_ptr<CInputStream> & input);
 };
 
-class DLL_LINKAGE CGeneralTextHandler //Handles general texts
+class CGeneralTextHandler;
+
+class DLL_LINKAGE LegacyTextContainer
+{
+	CGeneralTextHandler & owner;
+	std::string basePath;
+
+public:
+	LegacyTextContainer(CGeneralTextHandler & owner, std::string const & basePath);
+	const std::string & operator[](size_t index) const;
+};
+
+class DLL_LINKAGE LegacyHelpContainer
+{
+	CGeneralTextHandler & owner;
+	std::string basePath;
+
+public:
+	LegacyHelpContainer(CGeneralTextHandler & owner, std::string const & basePath);
+	std::pair<std::string, std::string> operator[](size_t index) const;
+};
+
+/// Handles all text-related data in game
+class DLL_LINKAGE CGeneralTextHandler
 {
+	/// map identifier -> localization
+	std::unordered_map<std::string, std::string> stringsLocalizations;
+
+	/// map localization -> identifier
+	std::unordered_map<std::string, std::string> stringsIdentifiers;
+
+	/// add selected string to internal storage
+	void registerString(const std::string & UID, const std::string & localized);
+	void registerH3String(const std::string & file, size_t index, const std::string & localized);
+
+	void readToVector(std::string sourceID, std::string sourceName);
+
 public:
-	JsonNode localizedTexts;
+	/// returns translated version of a string that can be displayed to user
+	const std::string & translate(const std::string & identifier) const;
+
+	/// returns translated version of a string that can be displayed to user, H3-array compatibility version
+	const std::string & translate(const std::string & identifier, size_t index) const;
+
+	/// converts translated string into locale-independent text that can be sent to another client
+	const std::string & serialize(const std::string & identifier) const;
+
+	/// converts identifier into user-readable string, may be identical to 'translate' but reserved for serialization calls
+	const std::string & deserialize(const std::string & identifier) const;
 
 	std::vector<std::string> allTexts;
 
-	std::vector<std::string> arraytxt;
-	std::vector<std::string> primarySkillNames;
-	std::vector<std::string> jktexts;
-	std::vector<std::string> heroscrn;
-	std::vector<std::string> overview;//text for Kingdom Overview window
-	std::vector<std::string> colors; //names of player colors ("red",...)
-	std::vector<std::string> capColors; //names of player colors with first letter capitalized ("Red",...)
-	std::vector<std::string> turnDurations; //turn durations for pregame (1 Minute ... Unlimited)
+	LegacyTextContainer arraytxt;
+	LegacyTextContainer primarySkillNames;
+	LegacyTextContainer jktexts;
+	LegacyTextContainer heroscrn;
+	LegacyTextContainer overview;//text for Kingdom Overview window
+	LegacyTextContainer colors; //names of player colors ("red",...)
+	LegacyTextContainer capColors; //names of player colors with first letter capitalized ("Red",...)
+	LegacyTextContainer turnDurations; //turn durations for pregame (1 Minute ... Unlimited)
 
 	//towns
-	std::vector<std::string> tcommands, hcommands, fcommands; //texts for town screen, town hall screen and fort screen
-	std::vector<std::string> tavernInfo;
-	std::vector<std::string> tavernRumors;
+	LegacyTextContainer tcommands, hcommands, fcommands; //texts for town screen, town hall screen and fort screen
+	LegacyTextContainer tavernInfo;
+	LegacyTextContainer tavernRumors;
 
-	std::vector<std::string> qeModCommands;
+	LegacyTextContainer qeModCommands;
 
-	std::vector<std::pair<std::string,std::string>> zelp;
-	std::vector<std::string> lossCondtions;
-	std::vector<std::string> victoryConditions;
+	LegacyHelpContainer zelp;
+	LegacyTextContainer lossCondtions;
+	LegacyTextContainer victoryConditions;
 
 	//objects
-	std::vector<std::string> creGens; //names of creatures' generators
-	std::vector<std::string> creGens4; //names of multiple creatures' generators
-	std::vector<std::string> advobtxt;
-	std::vector<std::string> xtrainfo;
-	std::vector<std::string> restypes; //names of resources
-	std::map<TerrainId, std::string> terrainNames;
-	std::vector<std::string> randsign;
-	std::vector<std::pair<std::string,std::string>> mines; //first - name; second - event description
+	LegacyTextContainer creGens; //names of creatures' generators
+	LegacyTextContainer creGens4; //names of multiple creatures' generators
+	LegacyTextContainer advobtxt;
+	LegacyTextContainer xtrainfo;
+	LegacyTextContainer restypes; //names of resources
+	LegacyTextContainer terrainNames;
+	LegacyTextContainer randsign;
 	std::vector<std::string> seerEmpty;
 	std::vector<std::vector<std::vector<std::string>>>  quests; //[quest][type][index]
 	//type: quest, progress, complete, rollover, log OR time limit //index: 0-2 seer hut, 3-5 border guard
 	std::vector<std::string> seerNames;
-	std::vector<std::string> tentColors;
+	LegacyTextContainer tentColors;
 
 	//sec skills
-	std::vector<std::string> levels;
+	LegacyTextContainer levels;
 	std::vector<std::string> zcrexp; //more or less useful content of that file
 	//commanders
 	std::vector<std::string> znpc00; //more or less useful content of that file
@@ -143,7 +187,7 @@ public:
 	std::vector<std::string> campaignMapNames;
 	std::vector<std::vector<std::string>> campaignRegionNames;
 
-	static void readToVector(std::string sourceName, std::vector<std::string> &dest);
+	std::vector<std::string> findStringsWithPrefix(std::string const & prefix);
 
 	int32_t pluralText(const int32_t textIndex, const int32_t count) const;
 

+ 1 - 1
lib/LogicalExpression.cpp

@@ -19,7 +19,7 @@ VCMI_LIB_NAMESPACE_BEGIN
 std::string LogicalExpressionDetail::getTextForOperator(std::string operation)
 {
 	//placed in cpp mostly to avoid unnecessary includes in header
-	return VLC->generaltexth->localizedTexts["logicalExpressions"][operation].String();
+	return VLC->generaltexth->translate("vcmi.logicalExpressions." + operation);
 }
 
 VCMI_LIB_NAMESPACE_END

+ 9 - 9
lib/mapObjects/CGTownInstance.cpp

@@ -1815,22 +1815,22 @@ const std::string CGTownBuilding::getVisitingBonusGreeting() const
 	switch(bType)
 	{
 	case BuildingSubID::MANA_VORTEX:
-		bonusGreeting = std::string(VLC->generaltexth->localizedTexts["townHall"]["greetingManaVortex"].String());
+		bonusGreeting = std::string(VLC->generaltexth->translate("vcmi.townHall.greetingManaVortex"));
 		break;
 	case BuildingSubID::KNOWLEDGE_VISITING_BONUS:
-		bonusGreeting = std::string(VLC->generaltexth->localizedTexts["townHall"]["greetingKnowledge"].String());
+		bonusGreeting = std::string(VLC->generaltexth->translate("vcmi.townHall.greetingKnowledge"));
 		break;
 	case BuildingSubID::SPELL_POWER_VISITING_BONUS:
-		bonusGreeting = std::string(VLC->generaltexth->localizedTexts["townHall"]["greetingSpellPower"].String());
+		bonusGreeting = std::string(VLC->generaltexth->translate("vcmi.townHall.greetingSpellPower"));
 		break;
 	case BuildingSubID::ATTACK_VISITING_BONUS:
-		bonusGreeting = std::string(VLC->generaltexth->localizedTexts["townHall"]["greetingAttack"].String());
+		bonusGreeting = std::string(VLC->generaltexth->translate("vcmi.townHall.greetingAttack"));
 		break;
 	case BuildingSubID::EXPERIENCE_VISITING_BONUS:
-		bonusGreeting = std::string(VLC->generaltexth->localizedTexts["townHall"]["greetingExperience"].String());
+		bonusGreeting = std::string(VLC->generaltexth->translate("vcmi.townHall.greetingExperience"));
 		break;
 	case BuildingSubID::DEFENSE_VISITING_BONUS:
-		bonusGreeting = std::string(VLC->generaltexth->localizedTexts["townHall"]["greetingDefence"].String());
+		bonusGreeting = std::string(VLC->generaltexth->translate("vcmi.townHall.greetingDefence"));
 		break;
 	}
 	auto buildingName = town->town->getSpecialBuilding(bType)->Name();
@@ -1849,12 +1849,12 @@ const std::string CGTownBuilding::getCustomBonusGreeting(const Bonus & bonus) co
 {
 	if(bonus.type == Bonus::TOWN_MAGIC_WELL)
 	{
-		auto bonusGreeting = std::string(VLC->generaltexth->localizedTexts["townHall"]["greetingInTownMagicWell"].String());
+		auto bonusGreeting = std::string(VLC->generaltexth->translate("vcmi.townHall.greetingInTownMagicWell"));
 		auto buildingName = town->town->getSpecialBuilding(bType)->Name();
 		boost::algorithm::replace_first(bonusGreeting, "%s", buildingName);
 		return bonusGreeting;
 	}
-	auto bonusGreeting = std::string(VLC->generaltexth->localizedTexts["townHall"]["greetingCustomBonus"].String()); //"%s gives you +%d %s%s"
+	auto bonusGreeting = std::string(VLC->generaltexth->translate("vcmi.townHall.greetingCustomBonus")); //"%s gives you +%d %s%s"
 	std::string param = "";
 	std::string until = "";
 
@@ -1864,7 +1864,7 @@ const std::string CGTownBuilding::getCustomBonusGreeting(const Bonus & bonus) co
 		param = VLC->generaltexth->allTexts[385];
 
 	until = bonus.duration == (ui16)Bonus::ONE_BATTLE
-		? VLC->generaltexth->localizedTexts["townHall"]["greetingCustomUntil"].String() : ".";
+		? VLC->generaltexth->translate("vcmi.townHall.greetingCustomUntil") : ".";
 
 	boost::format fmt = boost::format(bonusGreeting) % bonus.description % bonus.val % param % until;
 	std::string greeting = fmt.str();

+ 6 - 5
lib/mapObjects/MiscObjects.cpp

@@ -147,9 +147,8 @@ std::string CGCreature::getHoverText(const CGHeroInstance * hero) const
 		hoverName = getHoverText(hero->tempOwner);
 	}
 
-	const JsonNode & texts = VLC->generaltexth->localizedTexts["adventureMap"]["monsterThreat"];
+	hoverName += VLC->generaltexth->translate("vcmi.adventureMap.monsterThreat.title");
 
-	hoverName += texts["title"].String();
 	int choice;
 	double ratio = ((double)getArmyStrength() / hero->getTotalStrength());
 		 if (ratio < 0.1)  choice = 0;
@@ -164,7 +163,8 @@ std::string CGCreature::getHoverText(const CGHeroInstance * hero) const
 	else if (ratio < 8)    choice = 9;
 	else if (ratio < 20)   choice = 10;
 	else                   choice = 11;
-	hoverName += texts["levels"].Vector()[choice].String();
+
+	hoverName += VLC->generaltexth->translate("vcmi.adventureMap.monsterThreat.levels." + std::to_string(choice));
 	return hoverName;
 }
 
@@ -710,7 +710,7 @@ bool CGMine::isAbandoned() const
 
 std::string CGMine::getObjectName() const
 {
-	return VLC->generaltexth->mines.at(subID).first;
+	return VLC->generaltexth->translate("core.minename", subID);
 }
 
 std::string CGMine::getHoverText(PlayerColor player) const
@@ -1686,7 +1686,8 @@ void CGSignBottle::initObj(CRandomGenerator & rand)
 	//if no text is set than we pick random from the predefined ones
 	if(message.empty())
 	{
-		message = *RandomGeneratorUtil::nextItem(VLC->generaltexth->randsign, rand);
+		auto vector = VLC->generaltexth->findStringsWithPrefix("core.randsign");
+		message = *RandomGeneratorUtil::nextItem(vector, rand);
 	}
 
 	if(ID == Obj::OCEAN_BOTTLE)

+ 1 - 5
lib/mapping/CCampaignHandler.cpp

@@ -546,11 +546,7 @@ std::string CCampaignHandler::prologVideoName(ui8 index)
 std::string CCampaignHandler::prologMusicName(ui8 index)
 {
 	std::vector<std::string> music;
-
-	VLC->generaltexth->readToVector("Data/CmpMusic.txt", music);
-	if(index < music.size())
-		return music[index];
-	return "";
+	return VLC->generaltexth->translate("core.cmpmusic." + std::to_string(int(index)));
 }
 
 std::string CCampaignHandler::prologVoiceName(ui8 index)

+ 1 - 1
server/CGameHandler.cpp

@@ -2987,7 +2987,7 @@ bool CGameHandler::load(const std::string & filename)
 	catch(const CModHandler::Incompatibility & e)
 	{
 		logGlobal->error("Failed to load game: %s", e.what());
-		auto errorMsg = VLC->generaltexth->localizedTexts["server"]["errors"]["modsIncompatibility"].String() + '\n';
+		auto errorMsg = VLC->generaltexth->translate("vcmi.server.errors.modsIncompatibility") + '\n';
 		errorMsg += e.what();
 		lobby->announceMessage(errorMsg);
 		return false;