Browse Source

Merge pull request #4701 from IvanSavenko/localize_edited_strings

Localization improvements
Ivan Savenko 1 year ago
parent
commit
07e53b2fdf

+ 40 - 40
config/schemas/mod.json

@@ -5,6 +5,17 @@
 	"description" : "Format used to define main mod file (mod.json) in VCMI",
 	"required" : [ "name", "description", "modType", "version", "author", "contact" ],
 	"definitions" : {
+		"fileListOrObject" : {
+			"oneOf" : [
+				{
+					"type" : "array",
+					"items" : { "type" : "string", "format" : "textFile" }
+				},
+				{
+					"type" : "object"
+				}
+			]
+		},
 		"localizable" : {
 			"type" : "object",
 			"additionalProperties" : false,
@@ -35,9 +46,8 @@
 					"description" : "If set to true, vcmi will skip validation of current translation json files"
 				},
 				"translations" : {
-					"type" : "array",
 					"description" : "List of files with translations for this language",
-					"items" : { "type" : "string", "format" : "textFile" }
+					"$ref" : "#/definitions/fileListOrObject"
 				}
 			}
 		}
@@ -122,9 +132,17 @@
 			"description" : "If set to true, mod will not be enabled automatically on install"
 		},
 		"settings" : {
-			"type" : "object",
 			"description" : "List of changed game settings by mod",
-			"$ref" : "gameSettings.json"
+			"oneOf" : [
+				{
+					"type" : "object",
+					"$ref" : "gameSettings.json"
+				},
+				{
+					"type" : "array",
+					"items" : { "type" : "string", "format" : "textFile" }
+				},
+			]
 		},
 		"filesystem" : {
 			"type" : "object",
@@ -206,94 +224,76 @@
 			"$ref" : "#/definitions/localizable"
 		},
 		"translations" : {
-			"type" : "array",
 			"description" : "List of files with translations for this language",
-			"items" : { "type" : "string", "format" : "textFile" }
+			"$ref" : "#/definitions/fileListOrObject"
 		},
 		"factions" : {
-			"type" : "array",
 			"description" : "List of configuration files for towns/factions",
-			"items" : { "type" : "string", "format" : "textFile" }
+			"$ref" : "#/definitions/fileListOrObject"
 		},
 		"heroClasses" : {
-			"type" : "array",
 			"description" : "List of configuration files for hero classes",
-			"items" : { "type" : "string", "format" : "textFile" }
+			"$ref" : "#/definitions/fileListOrObject"
 		},
 		"heroes" : {
-			"type" : "array",
 			"description" : "List of configuration files for heroes",
-			"items" : { "type" : "string", "format" : "textFile" }
+			"$ref" : "#/definitions/fileListOrObject"
 		},
 		"skills" : {
-			"type" : "array",
 			"description" : "List of configuration files for skills",
-			"items" : { "type" : "string", "format" : "textFile" }
+			"$ref" : "#/definitions/fileListOrObject"
 		},
 		"creatures" : {
-			"type" : "array",
 			"description" : "List of configuration files for creatures",
-			"items" : { "type" : "string", "format" : "textFile" }
+			"$ref" : "#/definitions/fileListOrObject"
 		},
 		"artifacts" : {
-			"type" : "array",
 			"description" : "List of configuration files for artifacts",
-			"items" : { "type" : "string", "format" : "textFile" }
+			"$ref" : "#/definitions/fileListOrObject"
 		},
 		"spells" : {
-			"type" : "array",
 			"description" : "List of configuration files for spells",
-			"items" : { "type" : "string", "format" : "textFile" }
+			"$ref" : "#/definitions/fileListOrObject"
 		},
 		"objects" : {
-			"type" : "array",
 			"description" : "List of configuration files for objects",
-			"items" : { "type" : "string", "format" : "textFile" }
+			"$ref" : "#/definitions/fileListOrObject"
 		},
 		"biomes" : {
-			"type" : "array",
 			"description" : "List of configuration files for biomes",
-			"items" : { "type" : "string", "format" : "textFile" }
+			"$ref" : "#/definitions/fileListOrObject"
 		},
 		"bonuses" : {
-			"type" : "array",
 			"description" : "List of configuration files for bonuses",
-			"items" : { "type" : "string", "format" : "textFile" }
+			"$ref" : "#/definitions/fileListOrObject"
 		},
 		"terrains" : {
-			"type" : "array",
 			"description" : "List of configuration files for terrains",
-			"items" : { "type" : "string", "format" : "textFile" }
+			"$ref" : "#/definitions/fileListOrObject"
 		},
 		"roads" : {
-			"type" : "array",
 			"description" : "List of configuration files for roads",
-			"items" : { "type" : "string", "format" : "textFile" }
+			"$ref" : "#/definitions/fileListOrObject"
 		},
 		"rivers" : {
-			"type" : "array",
 			"description" : "List of configuration files for rivers",
-			"items" : { "type" : "string", "format" : "textFile" }
+			"$ref" : "#/definitions/fileListOrObject"
 		},
 		"battlefields" : {
-			"type" : "array",
 			"description" : "List of configuration files for battlefields",
-			"items" : { "type" : "string", "format" : "textFile" }
+			"$ref" : "#/definitions/fileListOrObject"
 		},
 		"obstacles" : {
-			"type" : "array",
 			"description" : "List of configuration files for obstacles",
-			"items" : { "type" : "string", "format" : "textFile" }
+			"$ref" : "#/definitions/fileListOrObject"
 		},
 		"templates" : {
-			"type" : "array",
 			"description" : "List of configuration files for RMG templates",
-			"items" : { "type" : "string", "format" : "textFile" }
+			"$ref" : "#/definitions/fileListOrObject"
 		},
 		"scripts" : {
-			"type" : "array",
 			"description" : "List of configuration files for scripts",
-			"items" : { "type" : "string", "format" : "textFile" }
+			"$ref" : "#/definitions/fileListOrObject"
 		}
 	}
 }

+ 3 - 2
docs/modders/Mod_File_Format.md

@@ -90,8 +90,9 @@ These are fields that are present only in local mod.json file
 {
 	// Following section describes configuration files with content added by mod
 	// It can be split into several files in any way you want but recommended organization is
-	// to keep one file per object (creature/hero/etc) and, if applicable, add separate file
-	// with translatable strings for each type of content
+	// to keep one file per object (creature/hero/etc)
+	// Alternatively, for small changes you can embed changes to content directly in here, e.g.
+	// "creatures" : { "core:imp" : { "health" : 5 }}
 
 	// list of factions/towns configuration files
 	"factions" :

+ 3 - 3
lib/CArtHandler.cpp

@@ -431,9 +431,9 @@ std::shared_ptr<CArtifact> CArtHandler::loadFromJson(const std::string & scope,
 
 	const JsonNode & text = node["text"];
 
-	VLC->generaltexth->registerString(scope, art->getNameTextID(), text["name"].String());
-	VLC->generaltexth->registerString(scope, art->getDescriptionTextID(), text["description"].String());
-	VLC->generaltexth->registerString(scope, art->getEventTextID(), text["event"].String());
+	VLC->generaltexth->registerString(scope, art->getNameTextID(), text["name"]);
+	VLC->generaltexth->registerString(scope, art->getDescriptionTextID(), text["description"]);
+	VLC->generaltexth->registerString(scope, art->getEventTextID(), text["event"]);
 
 	const JsonNode & graphics = node["graphics"];
 	art->image = graphics["image"].String();

+ 5 - 4
lib/CBonusTypeHandler.cpp

@@ -200,8 +200,9 @@ ImagePath CBonusTypeHandler::bonusToGraphics(const std::shared_ptr<Bonus> & bonu
 
 void CBonusTypeHandler::load()
 {
-	const JsonNode gameConf(JsonPath::builtin("config/gameConfig.json"));
-	const JsonNode config(JsonUtils::assembleFromFiles(gameConf["bonuses"].convertTo<std::vector<std::string>>()));
+	JsonNode gameConf(JsonPath::builtin("config/gameConfig.json"));
+	JsonNode config(JsonUtils::assembleFromFiles(gameConf["bonuses"].convertTo<std::vector<std::string>>()));
+	config.setModScope("vcmi");
 	load(config);
 }
 
@@ -240,8 +241,8 @@ void CBonusTypeHandler::loadItem(const JsonNode & source, CBonusType & dest, con
 
 	if (!dest.hidden)
 	{
-		VLC->generaltexth->registerString( "vcmi", dest.getNameTextID(), source["name"].String());
-		VLC->generaltexth->registerString( "vcmi", dest.getDescriptionTextID(), source["description"].String());
+		VLC->generaltexth->registerString( "vcmi", dest.getNameTextID(), source["name"]);
+		VLC->generaltexth->registerString( "vcmi", dest.getDescriptionTextID(), source["description"]);
 	}
 
 	const JsonNode & graphics = source["graphics"];

+ 3 - 3
lib/CCreatureHandler.cpp

@@ -617,9 +617,9 @@ std::shared_ptr<CCreature> CCreatureHandler::loadFromJson(const std::string & sc
 
 	cre->cost = ResourceSet(node["cost"]);
 
-	VLC->generaltexth->registerString(scope, cre->getNameSingularTextID(), node["name"]["singular"].String());
-	VLC->generaltexth->registerString(scope, cre->getNamePluralTextID(), node["name"]["plural"].String());
-	VLC->generaltexth->registerString(scope, cre->getDescriptionTextID(), node["description"].String());
+	VLC->generaltexth->registerString(scope, cre->getNameSingularTextID(), node["name"]["singular"]);
+	VLC->generaltexth->registerString(scope, cre->getNamePluralTextID(), node["name"]["plural"]);
+	VLC->generaltexth->registerString(scope, cre->getDescriptionTextID(), node["description"]);
 
 	cre->addBonus(node["hitPoints"].Integer(), BonusType::STACK_HEALTH);
 	cre->addBonus(node["speed"].Integer(), BonusType::STACKS_SPEED);

+ 5 - 5
lib/CHeroHandler.cpp

@@ -459,11 +459,11 @@ std::shared_ptr<CHero> CHeroHandler::loadFromJson(const std::string & scope, con
 	hero->onlyOnWaterMap = node["onlyOnWaterMap"].Bool();
 	hero->onlyOnMapWithoutWater = node["onlyOnMapWithoutWater"].Bool();
 
-	VLC->generaltexth->registerString(scope, hero->getNameTextID(), node["texts"]["name"].String());
-	VLC->generaltexth->registerString(scope, hero->getBiographyTextID(), node["texts"]["biography"].String());
-	VLC->generaltexth->registerString(scope, hero->getSpecialtyNameTextID(), node["texts"]["specialty"]["name"].String());
-	VLC->generaltexth->registerString(scope, hero->getSpecialtyTooltipTextID(), node["texts"]["specialty"]["tooltip"].String());
-	VLC->generaltexth->registerString(scope, hero->getSpecialtyDescriptionTextID(), node["texts"]["specialty"]["description"].String());
+	VLC->generaltexth->registerString(scope, hero->getNameTextID(), node["texts"]["name"]);
+	VLC->generaltexth->registerString(scope, hero->getBiographyTextID(), node["texts"]["biography"]);
+	VLC->generaltexth->registerString(scope, hero->getSpecialtyNameTextID(), node["texts"]["specialty"]["name"]);
+	VLC->generaltexth->registerString(scope, hero->getSpecialtyTooltipTextID(), node["texts"]["specialty"]["tooltip"]);
+	VLC->generaltexth->registerString(scope, hero->getSpecialtyDescriptionTextID(), node["texts"]["specialty"]["description"]);
 
 	hero->iconSpecSmall = node["images"]["specialtySmall"].String();
 	hero->iconSpecLarge = node["images"]["specialtyLarge"].String();

+ 2 - 2
lib/CSkillHandler.cpp

@@ -212,7 +212,7 @@ std::shared_ptr<CSkill> CSkillHandler::loadFromJson(const std::string & scope, c
 
 	skill->onlyOnWaterMap = json["onlyOnWaterMap"].Bool();
 
-	VLC->generaltexth->registerString(scope, skill->getNameTextID(), json["name"].String());
+	VLC->generaltexth->registerString(scope, skill->getNameTextID(), json["name"]);
 	switch(json["gainChance"].getType())
 	{
 	case JsonNode::JsonType::DATA_INTEGER:
@@ -237,7 +237,7 @@ std::shared_ptr<CSkill> CSkillHandler::loadFromJson(const std::string & scope, c
 			skill->addNewBonus(bonus, level);
 		}
 		CSkill::LevelInfo & skillAtLevel = skill->at(level);
-		VLC->generaltexth->registerString(scope, skill->getDescriptionTextID(level), levelNode["description"].String());
+		VLC->generaltexth->registerString(scope, skill->getDescriptionTextID(level), levelNode["description"]);
 		skillAtLevel.iconSmall = levelNode["images"]["small"].String();
 		skillAtLevel.iconMedium = levelNode["images"]["medium"].String();
 		skillAtLevel.iconLarge = levelNode["images"]["large"].String();

+ 1 - 1
lib/RiverHandler.cpp

@@ -50,7 +50,7 @@ std::shared_ptr<RiverType> RiverTypeHandler::loadFromJson(
 		info->paletteAnimation.push_back(element);
 	}
 
-	VLC->generaltexth->registerString(scope, info->getNameTextID(), json["text"].String());
+	VLC->generaltexth->registerString(scope, info->getNameTextID(), json["text"]);
 
 	return info;
 }

+ 1 - 1
lib/RoadHandler.cpp

@@ -41,7 +41,7 @@ std::shared_ptr<RoadType> RoadTypeHandler::loadFromJson(
 	info->shortIdentifier = json["shortIdentifier"].String();
 	info->movementCost    = json["moveCost"].Integer();
 
-	VLC->generaltexth->registerString(scope,info->getNameTextID(), json["text"].String());
+	VLC->generaltexth->registerString(scope,info->getNameTextID(), json["text"]);
 
 	return info;
 }

+ 1 - 1
lib/TerrainHandler.cpp

@@ -45,7 +45,7 @@ std::shared_ptr<TerrainType> TerrainTypeHandler::loadFromJson( const std::string
 	info->transitionRequired = json["transitionRequired"].Bool();
 	info->terrainViewPatterns = json["terrainViewPatterns"].String();
 
-	VLC->generaltexth->registerString(scope, info->getNameTextID(), json["text"].String());
+	VLC->generaltexth->registerString(scope, info->getNameTextID(), json["text"]);
 
 	const JsonVector & unblockedVec = json["minimapUnblocked"].Vector();
 	info->minimapUnblocked =

+ 5 - 5
lib/entities/faction/CTownHandler.cpp

@@ -292,8 +292,8 @@ void CTownHandler::loadBuilding(CTown * town, const std::string & stringID, cons
 	ret->modScope = source.getModScope();
 	ret->town = town;
 
-	VLC->generaltexth->registerString(source.getModScope(), ret->getNameTextID(), source["name"].String());
-	VLC->generaltexth->registerString(source.getModScope(), ret->getDescriptionTextID(), source["description"].String());
+	VLC->generaltexth->registerString(source.getModScope(), ret->getNameTextID(), source["name"]);
+	VLC->generaltexth->registerString(source.getModScope(), ret->getDescriptionTextID(), source["description"]);
 
 	ret->subId = vstd::find_or(MappedKeys::SPECIAL_BUILDINGS, source["type"].String(), BuildingSubID::NONE);
 	ret->resources = TResources(source["cost"]);
@@ -603,7 +603,7 @@ void CTownHandler::loadTown(CTown * town, const JsonNode & source)
 	town->namesCount = 0;
 	for(const auto & name : source["names"].Vector())
 	{
-		VLC->generaltexth->registerString(town->faction->modScope, town->getRandomNameTextID(town->namesCount), name.String());
+		VLC->generaltexth->registerString(town->faction->modScope, town->getRandomNameTextID(town->namesCount), name);
 		town->namesCount += 1;
 	}
 
@@ -718,8 +718,8 @@ std::shared_ptr<CFaction> CTownHandler::loadFromJson(const std::string & scope,
 	faction->modScope = scope;
 	faction->identifier = identifier;
 
-	VLC->generaltexth->registerString(scope, faction->getNameTextID(), source["name"].String());
-	VLC->generaltexth->registerString(scope, faction->getDescriptionTextID(), source["description"].String());
+	VLC->generaltexth->registerString(scope, faction->getNameTextID(), source["name"]);
+	VLC->generaltexth->registerString(scope, faction->getDescriptionTextID(), source["description"]);
 
 	faction->creatureBg120 = ImagePath::fromJson(source["creatureBackground"]["120px"]);
 	faction->creatureBg130 = ImagePath::fromJson(source["creatureBackground"]["130px"]);

+ 21 - 0
lib/json/JsonUtils.cpp

@@ -230,6 +230,27 @@ void JsonUtils::inherit(JsonNode & descendant, const JsonNode & base)
 	std::swap(descendant, inheritedNode);
 }
 
+JsonNode JsonUtils::assembleFromFiles(const JsonNode & files, bool & isValid)
+{
+	if (files.isVector())
+	{
+		auto configList = files.convertTo<std::vector<std::string> >();
+		JsonNode result = JsonUtils::assembleFromFiles(configList, isValid);
+
+		return result;
+	}
+	else
+	{
+		return files;
+	}
+}
+
+JsonNode JsonUtils::assembleFromFiles(const JsonNode & files)
+{
+	bool isValid = false;
+	return assembleFromFiles(files, isValid);
+}
+
 JsonNode JsonUtils::assembleFromFiles(const std::vector<std::string> & files)
 {
 	bool isValid = false;

+ 2 - 0
lib/json/JsonUtils.h

@@ -44,6 +44,8 @@ namespace JsonUtils
 	 * @brief generate one Json structure from multiple files
 	 * @param files - list of filenames with parts of json structure
 	 */
+	DLL_LINKAGE JsonNode assembleFromFiles(const JsonNode & files);
+	DLL_LINKAGE JsonNode assembleFromFiles(const JsonNode & files, bool & isValid);
 	DLL_LINKAGE JsonNode assembleFromFiles(const std::vector<std::string> & files);
 	DLL_LINKAGE JsonNode assembleFromFiles(const std::vector<std::string> & files, bool & isValid);
 

+ 1 - 1
lib/mapObjectConstructors/CBankInstanceConstructor.cpp

@@ -28,7 +28,7 @@ void CBankInstanceConstructor::initTypeData(const JsonNode & input)
 	if (input.Struct().count("name") == 0)
 		logMod->warn("Bank %s missing name!", getJsonKey());
 
-	VLC->generaltexth->registerString(input.getModScope(), getNameTextID(), input["name"].String());
+	VLC->generaltexth->registerString(input.getModScope(), getNameTextID(), input["name"]);
 
 	levels = input["levels"].Vector();
 	bankResetDuration = static_cast<si32>(input["resetDuration"].Float());

+ 1 - 1
lib/mapObjectConstructors/CObjectClassesHandler.cpp

@@ -278,7 +278,7 @@ std::unique_ptr<ObjectClass> CObjectClassesHandler::loadFromJson(const std::stri
 	newObject->base = json["base"];
 	newObject->id = index;
 
-	VLC->generaltexth->registerString(scope, newObject->getNameTextID(), json["name"].String());
+	VLC->generaltexth->registerString(scope, newObject->getNameTextID(), json["name"]);
 
 	newObject->objectTypeHandlers.resize(json["lastReservedIndex"].Float() + 1);
 

+ 1 - 1
lib/mapObjectConstructors/CRewardableConstructor.cpp

@@ -23,7 +23,7 @@ void CRewardableConstructor::initTypeData(const JsonNode & config)
 	blockVisit = config["blockedVisitable"].Bool();
 
 	if (!config["name"].isNull())
-		VLC->generaltexth->registerString( config.getModScope(), getNameTextID(), config["name"].String());
+		VLC->generaltexth->registerString( config.getModScope(), getNameTextID(), config["name"]);
 
 	JsonUtils::validate(config, "vcmi:rewardable", getJsonKey());
 	

+ 1 - 1
lib/mapObjectConstructors/DwellingInstanceConstructor.cpp

@@ -29,7 +29,7 @@ void DwellingInstanceConstructor::initTypeData(const JsonNode & input)
 	if (input.Struct().count("name") == 0)
 		logMod->warn("Dwelling %s missing name!", getJsonKey());
 
-	VLC->generaltexth->registerString( input.getModScope(), getNameTextID(), input["name"].String());
+	VLC->generaltexth->registerString( input.getModScope(), getNameTextID(), input["name"]);
 
 	const JsonVector & levels = input["creatures"].Vector();
 	const auto totalLevels = levels.size();

+ 2 - 2
lib/mapping/CMapHeader.cpp

@@ -189,7 +189,7 @@ void CMapHeader::registerMapStrings()
 		JsonUtils::mergeCopy(data, translations[language]);
 	
 	for(auto & s : data.Struct())
-		texts.registerString("map", TextIdentifier(s.first), s.second.String(), language);
+		texts.registerString("map", TextIdentifier(s.first), s.second.String());
 }
 
 std::string mapRegisterLocalizedString(const std::string & modContext, CMapHeader & mapHeader, const TextIdentifier & UID, const std::string & localized)
@@ -199,7 +199,7 @@ std::string mapRegisterLocalizedString(const std::string & modContext, CMapHeade
 
 std::string mapRegisterLocalizedString(const std::string & modContext, CMapHeader & mapHeader, const TextIdentifier & UID, const std::string & localized, const std::string & language)
 {
-	mapHeader.texts.registerString(modContext, UID, localized, language);
+	mapHeader.texts.registerString(modContext, UID, localized);
 	mapHeader.translations.Struct()[language].Struct()[UID.get()].String() = localized;
 	return UID.get();
 }

+ 5 - 41
lib/modding/CModHandler.cpp

@@ -384,7 +384,7 @@ std::set<TModID> CModHandler::getModDependencies(const TModID & modId, bool & is
 
 void CModHandler::initializeConfig()
 {
-	VLC->settingsHandler->loadBase(coreMod->config["settings"]);
+	VLC->settingsHandler->loadBase(JsonUtils::assembleFromFiles(coreMod->config["settings"]));
 
 	for(const TModID & modName : activeMods)
 	{
@@ -401,33 +401,6 @@ CModVersion CModHandler::getModVersion(TModID modName) const
 	return {};
 }
 
-bool CModHandler::validateTranslations(TModID modName) const
-{
-	bool result = true;
-	const auto & mod = allMods.at(modName);
-
-	{
-		auto fileList = mod.config["translations"].convertTo<std::vector<std::string> >();
-		JsonNode json = JsonUtils::assembleFromFiles(fileList);
-		result |= VLC->generaltexth->validateTranslation(mod.baseLanguage, modName, json);
-	}
-
-	for(const auto & language : Languages::getLanguageList())
-	{
-		if (mod.config[language.identifier].isNull())
-			continue;
-
-		if (mod.config[language.identifier]["skipValidation"].Bool())
-			continue;
-
-		auto fileList = mod.config[language.identifier]["translations"].convertTo<std::vector<std::string> >();
-		JsonNode json = JsonUtils::assembleFromFiles(fileList);
-		result |= VLC->generaltexth->validateTranslation(language.identifier, modName, json);
-	}
-
-	return result;
-}
-
 void CModHandler::loadTranslation(const TModID & modName)
 {
 	const auto & mod = allMods[modName];
@@ -435,14 +408,11 @@ void CModHandler::loadTranslation(const TModID & modName)
 	std::string preferredLanguage = VLC->generaltexth->getPreferredLanguage();
 	std::string modBaseLanguage = allMods[modName].baseLanguage;
 
-	auto baseTranslationList = mod.config["translations"].convertTo<std::vector<std::string> >();
-	auto extraTranslationList = mod.config[preferredLanguage]["translations"].convertTo<std::vector<std::string> >();
+	JsonNode baseTranslation = JsonUtils::assembleFromFiles(mod.config["translations"]);
+	JsonNode extraTranslation = JsonUtils::assembleFromFiles(mod.config[preferredLanguage]["translations"]);
 
-	JsonNode baseTranslation = JsonUtils::assembleFromFiles(baseTranslationList);
-	JsonNode extraTranslation = JsonUtils::assembleFromFiles(extraTranslationList);
-
-	VLC->generaltexth->loadTranslationOverrides(modBaseLanguage, modName, baseTranslation);
-	VLC->generaltexth->loadTranslationOverrides(preferredLanguage, modName, extraTranslation);
+	VLC->generaltexth->loadTranslationOverrides(modName, baseTranslation);
+	VLC->generaltexth->loadTranslationOverrides(modName, extraTranslation);
 }
 
 void CModHandler::load()
@@ -480,12 +450,6 @@ void CModHandler::load()
 	for(const TModID & modName : activeMods)
 		loadTranslation(modName);
 
-#if 0
-	for(const TModID & modName : activeMods)
-		if (!validateTranslations(modName))
-			allMods[modName].validation = CModInfo::FAILED;
-#endif
-
 	logMod->info("\tLoading mod data: %d ms", timer.getDiff());
 	VLC->creh->loadCrExpMod();
 	VLC->identifiersHandler->finalize();

+ 0 - 2
lib/modding/CModHandler.h

@@ -49,8 +49,6 @@ class DLL_LINKAGE CModHandler final : boost::noncopyable
 	void loadOneMod(std::string modName, const std::string & parent, const JsonNode & modSettings, bool enableMods);
 	void loadTranslation(const TModID & modName);
 
-	bool validateTranslations(TModID modName) const;
-
 	CModVersion getModVersion(TModID modName) const;
 
 public:

+ 2 - 2
lib/modding/ContentTypeHandler.cpp

@@ -50,7 +50,7 @@ ContentTypeHandler::ContentTypeHandler(IHandlerBase * handler, const std::string
 	}
 }
 
-bool ContentTypeHandler::preloadModData(const std::string & modName, const std::vector<std::string> & fileList, bool validate)
+bool ContentTypeHandler::preloadModData(const std::string & modName, const JsonNode & fileList, bool validate)
 {
 	bool result = false;
 	JsonNode data = JsonUtils::assembleFromFiles(fileList, result);
@@ -216,7 +216,7 @@ bool CContentHandler::preloadModData(const std::string & modName, JsonNode modCo
 	bool result = true;
 	for(auto & handler : handlers)
 	{
-		result &= handler.second.preloadModData(modName, modConfig[handler.first].convertTo<std::vector<std::string> >(), validate);
+		result &= handler.second.preloadModData(modName, modConfig[handler.first], validate);
 	}
 	return result;
 }

+ 1 - 1
lib/modding/ContentTypeHandler.h

@@ -39,7 +39,7 @@ public:
 
 	/// local version of methods in ContentHandler
 	/// returns true if loading was successful
-	bool preloadModData(const std::string & modName, const std::vector<std::string> & fileList, bool validate);
+	bool preloadModData(const std::string & modName, const JsonNode & fileList, bool validate);
 	bool loadMod(const std::string & modName, bool validate);
 	void loadCustom();
 	void afterLoadFinalization();

+ 1 - 1
lib/rewardable/Info.cpp

@@ -76,7 +76,7 @@ void Rewardable::Info::init(const JsonNode & objectConfig, const std::string & o
 
 	auto loadString = [&](const JsonNode & entry, const TextIdentifier & textID){
 		if (entry.isString() && !entry.String().empty() && entry.String()[0] != '@')
-			VLC->generaltexth->registerString(entry.getModScope(), textID, entry.String());
+			VLC->generaltexth->registerString(entry.getModScope(), textID, entry);
 	};
 
 	parameters = objectConfig;

+ 2 - 2
lib/spells/CSpellHandler.cpp

@@ -783,7 +783,7 @@ std::shared_ptr<CSpell> CSpellHandler::loadFromJson(const std::string & scope, c
 		spell->combat = type == "combat";
 	}
 
-	VLC->generaltexth->registerString(scope, spell->getNameTextID(), json["name"].String());
+	VLC->generaltexth->registerString(scope, spell->getNameTextID(), json["name"]);
 
 	logMod->trace("%s: loading spell %s", __FUNCTION__, spell->getNameTranslated());
 
@@ -1005,7 +1005,7 @@ std::shared_ptr<CSpell> CSpellHandler::loadFromJson(const std::string & scope, c
 		const si32 levelPower     = levelObject.power = static_cast<si32>(levelNode["power"].Integer());
 
 		if (!spell->isCreatureAbility())
-			VLC->generaltexth->registerString(scope, spell->getDescriptionTextID(levelIndex), levelNode["description"].String());
+			VLC->generaltexth->registerString(scope, spell->getDescriptionTextID(levelIndex), levelNode["description"]);
 
 		levelObject.cost          = static_cast<si32>(levelNode["cost"].Integer());
 		levelObject.AIValue       = static_cast<si32>(levelNode["aiValue"].Integer());

+ 0 - 5
lib/texts/CGeneralTextHandler.cpp

@@ -120,21 +120,16 @@ void CGeneralTextHandler::readToVector(const std::string & sourceID, const std::
 }
 
 CGeneralTextHandler::CGeneralTextHandler():
-	victoryConditions(*this, "core.vcdesc"   ),
-	lossConditions    (*this, "core.lcdesc"   ),
-	colors           (*this, "core.plcolors" ),
 	tcommands        (*this, "core.tcommand" ),
 	hcommands        (*this, "core.hallinfo" ),
 	fcommands        (*this, "core.castinfo" ),
 	advobtxt         (*this, "core.advevent" ),
 	restypes         (*this, "core.restypes" ),
-	randsign         (*this, "core.randsign" ),
 	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" ),

+ 0 - 5
lib/texts/CGeneralTextHandler.h

@@ -53,7 +53,6 @@ public:
 	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)
 
@@ -62,18 +61,14 @@ public:
 	LegacyTextContainer hcommands; // town hall screen
 	LegacyTextContainer fcommands; // fort screen
 	LegacyTextContainer tavernInfo;
-	LegacyTextContainer tavernRumors;
 
 	LegacyTextContainer qeModCommands;
 
 	LegacyHelpContainer zelp;
-	LegacyTextContainer lossConditions;
-	LegacyTextContainer victoryConditions;
 
 	//objects
 	LegacyTextContainer advobtxt;
 	LegacyTextContainer restypes; //names of resources
-	LegacyTextContainer randsign;
 	LegacyTextContainer seerEmpty;
 	LegacyTextContainer seerNames;
 	LegacyTextContainer tentColors;

+ 58 - 94
lib/texts/TextLocalizationContainer.cpp

@@ -22,20 +22,31 @@ VCMI_LIB_NAMESPACE_BEGIN
 
 std::recursive_mutex TextLocalizationContainer::globalTextMutex;
 
-void TextLocalizationContainer::registerStringOverride(const std::string & modContext, const std::string & language, const TextIdentifier & UID, const std::string & localized)
+void TextLocalizationContainer::registerStringOverride(const std::string & modContext, const TextIdentifier & UID, const std::string & localized)
 {
 	std::lock_guard globalLock(globalTextMutex);
 
 	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;
+	// load string override only in following cases:
+	// a) string was not modified in another mod (e.g. rebalance mod gave skill new description)
+	// b) this string override is defined in the same mod as one that provided modified version of this string
+	if (entry.identifierModContext == entry.baseStringModContext || modContext == entry.baseStringModContext)
+	{
+		entry.translatedText = localized;
+		if (entry.identifierModContext.empty())
+		{
+			entry.identifierModContext = modContext;
+			entry.baseStringModContext = modContext;
+		}
+	}
+	else
+	{
+		logGlobal->debug("Skipping translation override for string %s: changed in a different mod", UID.get());
+	}
 }
 
 void TextLocalizationContainer::addSubContainer(const TextLocalizationContainer & container)
@@ -55,7 +66,7 @@ void TextLocalizationContainer::removeSubContainer(const TextLocalizationContain
 	subContainers.erase(std::remove(subContainers.begin(), subContainers.end(), &container), subContainers.end());
 }
 
-const std::string & TextLocalizationContainer::deserialize(const TextIdentifier & identifier) const
+const std::string & TextLocalizationContainer::translateString(const TextIdentifier & identifier) const
 {
 	std::lock_guard globalLock(globalTextMutex);
 
@@ -63,108 +74,63 @@ const std::string & TextLocalizationContainer::deserialize(const TextIdentifier
 	{
 		for(auto containerIter = subContainers.rbegin(); containerIter != subContainers.rend(); ++containerIter)
 			if((*containerIter)->identifierExists(identifier))
-				return (*containerIter)->deserialize(identifier);
+				return (*containerIter)->translateString(identifier);
 
 		logGlobal->error("Unable to find localization for string '%s'", identifier.get());
 		return identifier.get();
 	}
 
 	const auto & entry = stringsLocalizations.at(identifier.get());
-
-	if (!entry.overrideValue.empty())
-		return entry.overrideValue;
-	return entry.baseValue;
+	return entry.translatedText;
 }
 
-void TextLocalizationContainer::registerString(const std::string & modContext, const TextIdentifier & UID, const std::string & localized, const std::string & language)
+void TextLocalizationContainer::registerString(const std::string & modContext, const TextIdentifier & inputUID, const JsonNode & localized)
 {
-	std::lock_guard globalLock(globalTextMutex);
-
-	assert(!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?
+	assert(localized.isNull() || !localized.getModScope().empty());
+	assert(localized.isNull() || !getModLanguage(localized.getModScope()).empty());
 
-	if(stringsLocalizations.count(UID.get()) > 0)
-	{
-		auto & value = stringsLocalizations[UID.get()];
-		value.baseLanguage = language;
-		value.baseValue = localized;
-	}
+	if (localized.isNull())
+		registerString(modContext, modContext, inputUID, localized.String());
 	else
-	{
-		StringState value;
-		value.baseLanguage = language;
-		value.baseValue = localized;
-		value.modContext = modContext;
-
-		stringsLocalizations[UID.get()] = value;
-	}
+		registerString(modContext, localized.getModScope(), inputUID, localized.String());
 }
 
 void TextLocalizationContainer::registerString(const std::string & modContext, const TextIdentifier & UID, const std::string & localized)
 {
-	assert(!getModLanguage(modContext).empty());
-	registerString(modContext, UID, localized, getModLanguage(modContext));
+	registerString(modContext, modContext, UID, localized);
 }
 
-bool TextLocalizationContainer::validateTranslation(const std::string & language, const std::string & modContext, const JsonNode & config) const
+void TextLocalizationContainer::registerString(const std::string & identifierModContext, const std::string & localizedStringModContext, const TextIdentifier & UID, const std::string & localized)
 {
 	std::lock_guard globalLock(globalTextMutex);
 
-	bool allPresent = true;
+	assert(!identifierModContext.empty());
+	assert(!localizedStringModContext.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 || identifierModContext == "map"); // registering already registered string?
 
-	for(const auto & string : stringsLocalizations)
+	if(stringsLocalizations.count(UID.get()) > 0)
 	{
-		if (string.second.modContext != modContext)
-			continue; // Not our mod
-
-		if (string.second.overrideLanguage == language)
-			continue; // Already translated
-
-		if (string.second.baseLanguage == language && !string.second.baseValue.empty())
-			continue; // Base string already uses our language
-
-		if (string.second.baseLanguage.empty())
-			continue; // String added in localization, not present in base language (e.g. maps/campaigns)
-
-		if (config.Struct().count(string.first) > 0)
-			continue;
-
-		if (allPresent)
-			logMod->warn("Translation into language '%s' in mod '%s' is incomplete! Missing lines:", language, modContext);
-
-		std::string currentText;
-		if (string.second.overrideValue.empty())
-			currentText = string.second.baseValue;
-		else
-			currentText = string.second.overrideValue;
-
-		logMod->warn(R"(    "%s" : "%s",)", string.first, TextOperations::escapeString(currentText));
-		allPresent = false;
+		auto & value = stringsLocalizations[UID.get()];
+		value.translatedText = localized;
+		value.identifierModContext = identifierModContext;
+		value.baseStringModContext = localizedStringModContext;
 	}
+	else
+	{
+		StringState value;
+		value.translatedText = localized;
+		value.identifierModContext = identifierModContext;
+		value.baseStringModContext = localizedStringModContext;
 
-	bool allFound = true;
-
-	//	for(const auto & string : config.Struct())
-	//	{
-	//		if (stringsLocalizations.count(string.first) > 0)
-	//			continue;
-	//
-	//		if (allFound)
-	//			logMod->warn("Translation into language '%s' in mod '%s' has unused lines:", language, modContext);
-	//
-	//		logMod->warn(R"(    "%s" : "%s",)", string.first, TextOperations::escapeString(string.second.String()));
-	//		allFound = false;
-	//	}
-
-	return allPresent && allFound;
+		stringsLocalizations[UID.get()] = value;
+	}
 }
 
-void TextLocalizationContainer::loadTranslationOverrides(const std::string & language, const std::string & modContext, const JsonNode & config)
+void TextLocalizationContainer::loadTranslationOverrides(const std::string & modContext, const JsonNode & config)
 {
 	for(const auto & node : config.Struct())
-		registerStringOverride(modContext, language, node.first, node.second.String());
+		registerStringOverride(modContext, node.first, node.second.String());
 }
 
 bool TextLocalizationContainer::identifierExists(const TextIdentifier & UID) const
@@ -184,17 +150,19 @@ void TextLocalizationContainer::exportAllTexts(std::map<std::string, std::map<st
 	for (auto const & entry : stringsLocalizations)
 	{
 		std::string textToWrite;
-		std::string modName = entry.second.modContext;
+		std::string modName = entry.second.baseStringModContext;
 
-		if (modName.find('.') != std::string::npos)
-			modName = modName.substr(0, modName.find('.'));
+		if (entry.second.baseStringModContext == entry.second.identifierModContext)
+		{
+			if (modName.find('.') != std::string::npos)
+				modName = modName.substr(0, modName.find('.'));
+		}
+		boost::range::replace(modName, '.', '_');
 
-		if (!entry.second.overrideValue.empty())
-			textToWrite = entry.second.overrideValue;
-		else
-			textToWrite = entry.second.baseValue;
+		textToWrite = entry.second.translatedText;
 
-		storage[modName][entry.first] = textToWrite;
+		if (!textToWrite.empty())
+			storage[modName][entry.first] = textToWrite;
 	}
 }
 
@@ -210,11 +178,7 @@ void TextLocalizationContainer::jsonSerialize(JsonNode & dest) const
 	std::lock_guard globalLock(globalTextMutex);
 
 	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;
-	}
+		dest.Struct()[s.first].String() = s.second.translatedText;
 }
 
 TextContainerRegistrable::TextContainerRegistrable()

+ 15 - 24
lib/texts/TextLocalizationContainer.h

@@ -23,26 +23,21 @@ protected:
 	struct StringState
 	{
 		/// Human-readable string that was added on registration
-		std::string baseValue;
-
-		/// Language of base string
-		std::string baseLanguage;
-
-		/// Translated human-readable string
-		std::string overrideValue;
-
-		/// Language of the override string
-		std::string overrideLanguage;
+		std::string translatedText;
 
 		/// ID of mod that created this string
-		std::string modContext;
+		std::string identifierModContext;
+
+		/// ID of mod that provides original, untranslated version of this string
+		/// Different from identifierModContext if mod has modified object from another mod (e.g. rebalance mods)
+		std::string baseStringModContext;
 
 		template <typename Handler>
 		void serialize(Handler & h)
 		{
-			h & baseValue;
-			h & baseLanguage;
-			h & modContext;
+			h & translatedText;
+			h & identifierModContext;
+			h & baseStringModContext;
 		}
 	};
 
@@ -52,7 +47,7 @@ protected:
 	std::vector<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);
+	void registerStringOverride(const std::string & modContext, const TextIdentifier & UID, const std::string & localized);
 
 	std::string getModLanguage(const std::string & modContext);
 
@@ -60,29 +55,25 @@ protected:
 	bool identifierExists(const TextIdentifier & UID) const;
 
 public:
-	/// validates translation of specified language for specified mod
-	/// returns true if localization is valid and complete
-	/// any error messages will be written to log file
-	bool validateTranslation(const std::string & language, const std::string & modContext, JsonNode const & file) const;
-
 	/// Loads translation from provided json
 	/// 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);
+	void loadTranslationOverrides(const std::string & modContext, JsonNode const & file);
 
 	/// add selected string to internal storage
+	void registerString(const std::string & modContext, const TextIdentifier & UID, const JsonNode & localized);
 	void registerString(const std::string & modContext, const TextIdentifier & UID, const std::string & localized);
-	void registerString(const std::string & modContext, const TextIdentifier & UID, const std::string & localized, const std::string & language);
+	void registerString(const std::string & identifierModContext, const std::string & localizedStringModContext, const TextIdentifier & UID, const std::string & localized);
 
 	/// returns translated version of a string that can be displayed to user
 	template<typename  ... Args>
 	std::string translate(std::string arg1, Args ... args) const
 	{
 		TextIdentifier id(arg1, args ...);
-		return deserialize(id);
+		return translateString(id);
 	}
 
 	/// converts identifier into user-readable string
-	const std::string & deserialize(const TextIdentifier & identifier) const;
+	const std::string & translateString(const TextIdentifier & identifier) const;
 
 	/// Debug method, returns all currently stored texts
 	/// Format: [mod ID][string ID] -> human-readable text