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
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;
}