Determine which string table entries are in use

I made a solution Unreal 4.27.2. Keep in mind if u run this will cause unreal to hitch as it needs to load the assets to check FTexts. There are quite a few methods needed here.

Main one is GatherLocalizationInfoOnObjectType - which will give u a nice log of all the texts, and will check if there is a matching text in a libaraby that is simple not connected.

Besides that I just made a small untest func GatherAllUnusedTextsFromLocalTables which will give u LibaryStringTexts that are not referenced anywhere. Meaning they could be deleted from translation list or need to be implmented.

TODO GetAllFTexts - could also search arrays and struct upropertys, didnt add that

Unreal 4.27.2 GL HF bois :smiley:


In your .h some structs 

USTRUCT(BlueprintType)
struct FBlueprintClassSearchInfo
{
	GENERATED_BODY()

public:
	FBlueprintClassSearchInfo(UClass* Class_ = nullptr, FString BlueprintName_ = "") : Class(Class_), BlueprintName(BlueprintName_) {}

	UPROPERTY(BlueprintReadWrite, EditAnywhere)
	UClass* Class;

	//because getting name of class just gives us "GeneratedClass"
	UPROPERTY(BlueprintReadWrite, EditAnywhere)
	FString BlueprintName;
};

USTRUCT(BlueprintType)
struct FTextPropertyInfo
{
	GENERATED_BODY()

public:
	FTextPropertyInfo(UClass* Class_ = nullptr, FString TextVariableName_ = "", FText TextVariableValue_ = FText::FromString(""))
	:Class(Class_), TextVariableName(TextVariableName_), TextVariableValue(TextVariableValue_){}

	UPROPERTY(BlueprintReadWrite, EditAnywhere)
	UClass* Class;
	
	UPROPERTY(BlueprintReadWrite, EditAnywhere)
	FString TextVariableName;

	UPROPERTY(BlueprintReadWrite, EditAnywhere)
	FText TextVariableValue;
};



USTRUCT(BlueprintType)
struct FLocalizationCheckInfo
{
	GENERATED_BODY()

public:
	FLocalizationCheckInfo(bool bIsConnectedToStringTable_ = false, bool bFoundMatchTextInStringTable_ = false, FString TextVariableName_ = "", FString TextVariableValue_ = "", FString FoundKey_ = "", FBlueprintClassSearchInfo BpSearchInfo_ = FBlueprintClassSearchInfo())
	: bIsConnectedToStringTable(bIsConnectedToStringTable_),bFoundMatchTextInStringTable(bFoundMatchTextInStringTable_), TextVariableName(TextVariableName_), TextVariableValue(TextVariableValue_), FoundKey(FoundKey_), BpSearchInfo(BpSearchInfo_){}

	UPROPERTY(BlueprintReadWrite, EditAnywhere)
	bool bIsConnectedToStringTable;

	UPROPERTY(BlueprintReadWrite, EditAnywhere)
	bool bFoundMatchTextInStringTable;

	UPROPERTY(BlueprintReadWrite, EditAnywhere)
	FString TextVariableName;

	UPROPERTY(BlueprintReadWrite, EditAnywhere)
	FString TextVariableValue;

	UPROPERTY(BlueprintReadWrite, EditAnywhere)
	FString FoundKey;
	
	UPROPERTY(BlueprintReadWrite, EditAnywhere)
	FBlueprintClassSearchInfo BpSearchInfo;
};


and .cpp

TArray<FLocalizationCheckInfo> UAIP_Utils::GatherLocalizationInfoOnObjectType(TSubclassOf<UObject> ObjectType, FString OptionalPath, bool bPrintLogUnlocalizedTexts)
{
	//collect all blueprint subclasses of type widget	
	TArray<FBlueprintClassSearchInfo> BlueprintClassSearchResults;
	GetAllBlueprintSubclasses(ObjectType,BlueprintClassSearchResults, OptionalPath);
	
	TArray<FLocalizationCheckInfo> OutResults;
	
	for (FBlueprintClassSearchInfo Info : BlueprintClassSearchResults)
	{
		//common ones in widgets we dont need, also ignore emptys
		TArray<FString> IgnoreTexts = {TEXT(""), TEXT("Tooltip"), TEXT("PaletteCategory")};
		TArray<FTextPropertyInfo> FoundTexts = GetAllFTexts(Info.Class, IgnoreTexts);

		for (FTextPropertyInfo Text : FoundTexts)
		{
			if (!UKismetTextLibrary::TextIsFromStringTable(Text.TextVariableValue))
			{
				//check if text exists in string table (just not connected"!) // often happens by accident text was connected then clicked on once and it disconnects.
				TArray<FName> AllStringTableID = UKismetStringTableLibrary::GetRegisteredStringTables();

				bool bFound = false;
				
				for (FName StringTableID : AllStringTableID)
				{
					TArray<FString> AllStringsInTable = UKismetStringTableLibrary::GetKeysFromStringTable(StringTableID);

					for (FString TableKey: AllStringsInTable)
					{
						FString FindLocalOutText;
						FindLocalizedText(UKismetStringTableLibrary::GetTableNamespace(StringTableID),TableKey,FindLocalOutText);
						
						//we often have pattern we manually wrote in text and without "."
						if (FindLocalOutText.Equals(Text.TextVariableValue.ToString(),ESearchCase::IgnoreCase) || FindLocalOutText.Equals(Text.TextVariableValue.ToString() + ".",ESearchCase::IgnoreCase))
						{
							bFound = true;
							OutResults.Add(FLocalizationCheckInfo(false, true, Text.TextVariableName, Text.TextVariableValue.ToString(),TableKey,Info));
							break;
						}
					}
				}

				if (!bFound)
					OutResults.Add(FLocalizationCheckInfo(false, false, Text.TextVariableName, Text.TextVariableValue.ToString(),"",Info));
			} else
			{
				OutResults.Add(FLocalizationCheckInfo(true, true, Text.TextVariableName, Text.TextVariableValue.ToString(),"",Info));
			}
		}
	}
	
	//3 Log to screen and logfile
	if (bPrintLogUnlocalizedTexts)
	{
		FString LogString;
	
		for (FLocalizationCheckInfo LocalInfo: OutResults)
		{
			if (!LocalInfo.bIsConnectedToStringTable)
			{
				FString LocalInfoString =  LocalInfo.BpSearchInfo.BlueprintName + "	   Variable name: " + LocalInfo.TextVariableName + "	Variable Value: " + LocalInfo.TextVariableValue + "   Match in string table " + UKismetStringLibrary::Conv_BoolToString(LocalInfo.bFoundMatchTextInStringTable) + "      Text ID Found " + LocalInfo.FoundKey;
				LogString.Append(LocalInfoString + "\n");
			}
		}
			
		const FString FilePath = FPaths::ConvertRelativePathToFull(FPaths::ProjectSavedDir()) + TEXT("/Logs/UnlocalizedTexts.txt");
		FFileHelper::SaveStringToFile(LogString, *FilePath, FFileHelper::EEncodingOptions::AutoDetect, &IFileManager::Get(), EFileWrite::FILEWRITE_Append);
	
		UE_LOG(LogTemp,Warning,TEXT(" \n %s"),*LogString);
	}
	
	return OutResults;
}

bool UAIP_Utils::FindLocalizedText(const FString& Namespace, const FString& Key, FString& OutText)
{
	FTextDisplayStringPtr FoundString = FTextLocalizationManager::Get().FindDisplayString(Namespace, Key);
	if (FoundString.IsValid())
	{
		OutText = *FoundString;
		return true;
	}
	return false;
}

TArray<FTextPropertyInfo> UAIP_Utils::GetAllFTexts(TSubclassOf<UObject> ObjectType, TArray<FString> IgnoreTextsVariables, bool bIgnoreEmpty)
{
	//todo could part structs and arrays for texts as well in theory
	TArray <FTextPropertyInfo> OutArray;
	for (TFieldIterator<FTextProperty> Property(ObjectType); Property; ++Property)
	{
		const FString TextVariableName = Property->GetFName().GetPlainNameString();
		const FText& TextVariableValue = Property->GetPropertyValue_InContainer(ObjectType->GetDefaultObject());

		bool bIgnore = false;
		
		for (FString IgnoreName: IgnoreTextsVariables)
		{
			if (IgnoreName.Equals(TextVariableName,ESearchCase::IgnoreCase))
			{
				bIgnore = true;
				break;
			}
		}

		if (bIgnoreEmpty)
		{
			if (TextVariableValue.ToString() == "" || TextVariableValue.ToString() == "Artifacts")
			{
				bIgnore = true;
			}
		}
		
		if (!bIgnore)
			OutArray.Add(FTextPropertyInfo(ObjectType,TextVariableName,TextVariableValue));
	}
	
	return OutArray;
}

void UAIP_Utils::GetAllBlueprintSubclasses(UClass* BaseClass, TArray<FBlueprintClassSearchInfo>& BlueprintSearchResults, FString OptionalPath = "")
{
	FName BaseClassName = BaseClass->GetFName();
	//UE_LOG(LogTemp, Log, TEXT("Getting all blueprint subclasses of '%s'"), *BaseClassName.ToString());

	FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
	IAssetRegistry& AssetRegistry = AssetRegistryModule.Get();
	TArray<FAssetData> AssetData;
	// The asset registry is populated asynchronously at startup, so there's no guarantee it has finished.
	// This simple approach just runs a synchronous scan on the entire content directory.
	// Better solutions would be to specify only the path to where the relevant blueprints are,
	// or to register a callback with the asset registry to be notified of when it's finished populating.
	TArray< FString > ContentPaths;
	
	ContentPaths.Add(TEXT("/Game") + OptionalPath);
	AssetRegistry.ScanPathsSynchronous(ContentPaths);

	// Use the asset registry to get the set of all class names deriving from Base
	TSet< FName > DerivedNames;
	{
		TArray< FName > BaseNames;
		BaseNames.Add(BaseClassName);

		TSet< FName > Excluded;
        AssetRegistry.GetDerivedClassNames(BaseNames, Excluded, DerivedNames);
		// AssetRegistry.GetDerivedClassNames(BaseNames, Excluded, DerivedNames);
	}
	
	FARFilter Filter;
    //Filter.ClassPaths.Add(UBlueprint::StaticClass()->GetClassPathName());
	Filter.ClassNames.Add(UBlueprint::StaticClass()->GetFName());
	Filter.bRecursiveClasses = true;
	Filter.bRecursivePaths = true;

	TArray< FAssetData > AssetList;
	AssetRegistry.GetAssets(Filter, AssetList);
	
	// Iterate over retrieved blueprint assets
	for(auto const& Asset : AssetList) {
		
		// Get the the class this blueprint generates (this is stored as a full path)
        auto GeneratedClassPathPtr = Asset.TagsAndValues.FindTag(TEXT("GeneratedClass")).AsString();
		if(!GeneratedClassPathPtr.IsEmpty()) {

			//UE_LOG(LogTemp,Warning, TEXT("PrintLogOfAllUnlocalizedTexts 1"));
			
			// Convert path to just the name part
			const FString ClassObjectPath = FPackageName::ExportTextPathToObjectPath(*GeneratedClassPathPtr);
			const FString ObjectClassName = FPackageName::ObjectPathToObjectName(ClassObjectPath);
			
			// Check if this class is in the derived set
			if(!DerivedNames.Contains(*ObjectClassName)) {
				
				continue;
			}
			
			UClass* Class = nullptr;
			//load asset
			const UBlueprint* BlueprintAsset = Cast<UBlueprint>(Asset.GetAsset());
			if (BlueprintAsset) {
				Class = BlueprintAsset->GeneratedClass;
			} else {
				
			}
			if (Class) {
				BlueprintSearchResults.Add(FBlueprintClassSearchInfo(Class, ObjectClassName.LeftChop(2)));
			} else {
				
			}
		}
	}
}

//note will cause hitch
TArray<FLocalizationCheckInfo> UAIP_Utils::GatherAllUnusedTextsFromLocalTables()
{
	TArray<FLocalizationCheckInfo> GatheredInfo = GatherLocalizationInfoOnObjectType(UObject::StaticClass(),"", false);
		
	//gather just all keys and string from all tables.
	TArray<FLocalizationCheckInfo> OutAllUnusedLocalInfo;

	TArray<FName> AllStringTableID = UKismetStringTableLibrary::GetRegisteredStringTables();
	for (FName StringTableID : AllStringTableID)
	{
		TArray<FString> AllStringsInTable = UKismetStringTableLibrary::GetKeysFromStringTable(StringTableID);

		for (FString TableKey: AllStringsInTable)
		{	
			FString FindLocalOutText;

			//for now ignore other tables we want to test with text
			if (!UKismetStringTableLibrary::GetTableNamespace(StringTableID).Contains("TEXT"))
				continue;
			
			FindLocalizedText(UKismetStringTableLibrary::GetTableNamespace(StringTableID),TableKey,FindLocalOutText);
			OutAllUnusedLocalInfo.Add(FLocalizationCheckInfo(true,true,"",FindLocalOutText,TableKey, FBlueprintClassSearchInfo()));
		}
	}
	
	for (FLocalizationCheckInfo Info :GatheredInfo)
	{
		if (Info.bIsConnectedToStringTable || Info.bFoundMatchTextInStringTable)
		{
			int FoundIndex = -1;

			for (int i = 0; i< OutAllUnusedLocalInfo.Num(); i++)
			{
				if (OutAllUnusedLocalInfo[i].TextVariableValue == Info.TextVariableValue)
				{	
					FoundIndex = i;
					break;
				}
			}
			
			if (FoundIndex > -1)
			{
				OutAllUnusedLocalInfo.RemoveAt(FoundIndex);
			}
		}
	}
	
	//3 Log to screen and logfile
	FString LogString;
	
	for (FLocalizationCheckInfo LocalInfo: OutAllUnusedLocalInfo)
	{
		FString LocalInfoString = "Unused Local Text Key: " + LocalInfo.FoundKey + " Variable value: " + LocalInfo.TextVariableValue;
		LogString.Append(LocalInfoString + "\n");
	}
			
	const FString FilePath = FPaths::ConvertRelativePathToFull(FPaths::ProjectSavedDir()) + TEXT("/Logs/UsedTextFromStringLibrary.txt");
	FFileHelper::SaveStringToFile(LogString, *FilePath, FFileHelper::EEncodingOptions::AutoDetect, &IFileManager::Get(), EFileWrite::FILEWRITE_Append);
	
	UE_LOG(LogTemp,Warning,TEXT("\n %s"),*LogString);
	
	return OutAllUnusedLocalInfo;
}