diff --git a/Source/EasyLocalizationToolEditor/EasyLocalizationToolEditor.Build.cs b/Source/EasyLocalizationToolEditor/EasyLocalizationToolEditor.Build.cs index 4f3d1f5..165912f 100644 --- a/Source/EasyLocalizationToolEditor/EasyLocalizationToolEditor.Build.cs +++ b/Source/EasyLocalizationToolEditor/EasyLocalizationToolEditor.Build.cs @@ -40,6 +40,10 @@ public EasyLocalizationToolEditor(ReadOnlyTargetRules Target) : base(Target) "EditorWidgets", "BlueprintGraph", "PropertyEditor", + "ContentBrowser", + "AssetRegistry", + "Kismet", + "ApplicationCore", } ); @@ -98,4 +102,4 @@ public EasyLocalizationToolEditor(ReadOnlyTargetRules Target) : base(Target) PublicDefinitions.Add("ELTEDITOR_USE_SLATE_EDITOR_UI=0"); } } -} +} \ No newline at end of file diff --git a/Source/EasyLocalizationToolEditor/Private/ELTCommandlet.cpp b/Source/EasyLocalizationToolEditor/Private/ELTCommandlet.cpp index 5a93910..6ab23e6 100644 --- a/Source/EasyLocalizationToolEditor/Private/ELTCommandlet.cpp +++ b/Source/EasyLocalizationToolEditor/Private/ELTCommandlet.cpp @@ -37,11 +37,13 @@ int32 UELTCommandlet::Main(const FString& Params) FString Fallback = TEXT("NONE"); FParse::Value(*Params, TEXT("-Fallback="), Fallback); + bool bGenerateStringTables = FParse::Param(*Params, TEXT("-GenStringTables")); + const FString LocName = FPaths::GetBaseFilename(LocPath); // Run generation of loc files implementation. Get the output message and display it the localization fails. FString OutMessage; - if (UELTEditor::GenerateLocFilesImpl(CSVPath, LocPath, LocName, Namespace, Separator, Fallback, OutMessage) == false) + if (UELTEditor::GenerateLocFilesImpl(CSVPath, LocPath, LocName, Namespace, Separator, Fallback, bGenerateStringTables, OutMessage) == false) { UE_LOG(ELTCommandletLog, Log, TEXT("+++ Failed to generate Localization: %s"), *OutMessage); return 1; diff --git a/Source/EasyLocalizationToolEditor/Private/ELTEditor.cpp b/Source/EasyLocalizationToolEditor/Private/ELTEditor.cpp index e4dfc65..feb1bdd 100644 --- a/Source/EasyLocalizationToolEditor/Private/ELTEditor.cpp +++ b/Source/EasyLocalizationToolEditor/Private/ELTEditor.cpp @@ -1,14 +1,19 @@ // Copyright (c) 2026 Damian Nowakowski. All rights reserved. #include "ELTEditor.h" +#include "AssetToolsModule.h" #include "Internationalization/TextLocalizationResource.h" #include "Internationalization/TextLocalizationManager.h" +#include "Internationalization/StringTableCore.h" +#include "Internationalization/StringTable.h" #include "Misc/FileHelper.h" #include "Misc/Paths.h" #include "Misc/MessageDialog.h" #include "ELTEditorSettings.h" #include "ELTEditorWidget.h" +#include "ELTEditorAuditWidget.h" #include "ELTSettings.h" +#include "UObject/SavePackage.h" #if ((ENGINE_MAJOR_VERSION == 5) && (ENGINE_MINOR_VERSION >= 1)) #include "AssetRegistry/AssetRegistryModule.h" @@ -37,6 +42,10 @@ void UELTEditor::Init() CSVPaths = UELTEditorSettings::GetCSVPaths(); CurrentLocPath = UELTEditorSettings::GetLocalizationPath(); + // Bind the audit widget's Reimport CSV button to this editor's generate function. + // Done here rather than InitializeTheWidget, as that requires ELT tool widget opened. + UELTEditorAuditWidget::OnReimportCSVDelegate.BindUObject(this, &UELTEditor::OnGenerateLocFiles); + // Reimport localizations (if this option is enabled). if (UELTEditorSettings::GetReimportAtEditorStartup()) { @@ -45,7 +54,7 @@ void UELTEditor::Init() } // Refresh information about available localizations. - RefreshAvailableLangs(false); + RefreshAvailableLangs(ERefreshUIFlags::None); // Set preview language (if the option is enabled). SetLanguagePreview(); @@ -147,7 +156,7 @@ void UELTEditor::ChangeTabWorld(UWorld* World, EMapChangeType MapChangeType) void UELTEditor::InitializeTheWidget() { // Check available languages (based on files in Localization directory) - RefreshAvailableLangs(true); + RefreshAvailableLangs(ERefreshUIFlags::All); // Bind all required delegates to the Widget. EditorWidget->OnLocalizationPathSelectedDelegate.BindUObject(this, &UELTEditor::OnLocalizationPathChanged); @@ -162,6 +171,7 @@ void UELTEditor::InitializeTheWidget() EditorWidget->OnGlobalNamespaceChangedDelegate.BindUObject(this, &UELTEditor::OnGlobalNamespaceChanged); EditorWidget->OnSeparatorChangedDelegate.BindUObject(this, &UELTEditor::OnSeparatorChanged); EditorWidget->OnFallbackWhenEmptyChangedDelegate.BindUObject(this, &UELTEditor::OnFallbackWhenEmptyChanged); + EditorWidget->OnGenerateKeyReferenceStringTableChangedDelegate.BindUObject(this, &UELTEditor::OnGenerateKeyReferenceStringTableChanged); EditorWidget->OnLogDebugChangedDelegate.BindUObject(this, &UELTEditor::OnLogDebugChanged); EditorWidget->OnPreviewInUIChangedDelegate.BindUObject(this, &UELTEditor::OnPreviewInUIChanged); @@ -198,6 +208,9 @@ void UELTEditor::InitializeTheWidget() EditorWidget->CallSetLocalizationOnFirstRun(UELTSettings::GetOverrideLanguageAtFirstLaunch()); EditorWidget->CallSetLocalizationOnFirstRunLang(UELTSettings::GetLanguageToOverrideAtFirstLaunch()); + // Set the Generate Key Reference String Table current value to the Widget. + EditorWidget->CallSetGenerateKeyReferenceStringTable(UELTEditorSettings::GetGenerateKeyReferenceStringTable()); + // Set LogDebug value to the Widget. EditorWidget->CallSetLogDebug(UELTEditorSettings::GetLogDebug()); @@ -236,7 +249,7 @@ void UELTEditor::OnLocalizationPathChanged(const FString& NewPath) EditorWidget->CallFillCSVPath(PathsStringToList(GetCurrentCSVPath())); // Refresh available languages for this Localization directory and set them to the Widget. - RefreshAvailableLangs(false); + RefreshAvailableLangs(ERefreshUIFlags::None); EditorWidget->CallFillAvailableLangsInLocFile(CurrentAvailableLangsForLocFile); // Set Global Namespace for this Localization directory to the Widget. @@ -266,12 +279,13 @@ void UELTEditor::OnGenerateLocFiles() const bool bSuccess = GenerateLocFiles(ReturnMessage); if (bSuccess) { - RefreshAvailableLangs(true); + const ERefreshUIFlags Flags = EditorWidget ? ERefreshUIFlags::All : ERefreshUIFlags::AuditWidget; + RefreshAvailableLangs(Flags); SetLanguagePreview(); } - + // Display a Dialog Window to inform user that the localization generation has been finished. -#if (ENGINE_MAJOR_VERSION == 5) +#if (ENGINE_MAJOR_VERSION == 5) && ENGINE_MINOR_VERSION >= 3 FMessageDialog::Open((bSuccess ? EAppMsgCategory::Success : EAppMsgCategory::Error), EAppMsgType::Ok, FText::FromString(ReturnMessage)); #else FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(ReturnMessage)); @@ -357,6 +371,11 @@ void UELTEditor::OnFallbackWhenEmptyChanged(const FString& NewFallback) UELTEditorSettings::SetFallbackWhenEmpty(NewFallback); } +void UELTEditor::OnGenerateKeyReferenceStringTableChanged(bool bNewGenerateKeyReferenceStringTable) +{ + UELTEditorSettings::SetGenerateKeyReferenceStringTable(bNewGenerateKeyReferenceStringTable); +} + void UELTEditor::OnLogDebugChanged(bool bNewLogDebug) { // "Log Debug" flag has been changed in the Widget. Save this setting. @@ -385,7 +404,7 @@ void UELTEditor::SetLanguagePreview() } } -void UELTEditor::RefreshAvailableLangs(bool bRefreshUI) +void UELTEditor::RefreshAvailableLangs(ERefreshUIFlags UIFlags) { // Get all available languages by reading the localization directory. // Languages in current localization directory put into the separate array too. @@ -413,7 +432,10 @@ void UELTEditor::RefreshAvailableLangs(bool bRefreshUI) } } - if (bRefreshUI) + const bool bRefreshELTWidget = EnumHasAnyFlags(UIFlags, ERefreshUIFlags::ToolWidget); + const bool bRefreshAuditWidget = EnumHasAnyFlags(UIFlags, ERefreshUIFlags::AuditWidget); + + if (bRefreshELTWidget && EditorWidget) { // If the RefreshUI has been requested - set the available languages on the Widget. EditorWidget->CallFillAvailableLangs(CurrentAvailableLangs); @@ -444,6 +466,11 @@ void UELTEditor::RefreshAvailableLangs(bool bRefreshUI) } } + if (bRefreshAuditWidget) + { + UELTEditorAuditWidget::UpdateAvailableLanguages(CurrentAvailableLangs); + } + // Set available languages to the game settings. UELTSettings::SetAvailableLanguages(CurrentAvailableLangs); } @@ -465,12 +492,12 @@ bool UELTEditor::GenerateLocFiles(FString& OutMessage) } const TArray& CSVFilePaths = PathsStringToList(GetCurrentCSVPath()); const FString LocPath = FPaths::ConvertRelativePathToFull(CurrentLocPath); - return GenerateLocFilesImpl(CSVFilePaths, LocPath, GetCurrentLocName(), GetCurrentGlobalNamespace(), UELTEditorSettings::GetSeparator(), UELTEditorSettings::GetFallbackWhenEmpty(), OutMessage); + return GenerateLocFilesImpl(CSVFilePaths, LocPath, GetCurrentLocName(), GetCurrentGlobalNamespace(), UELTEditorSettings::GetSeparator(), UELTEditorSettings::GetFallbackWhenEmpty(), UELTEditorSettings::GetGenerateKeyReferenceStringTable(), OutMessage); } -bool UELTEditor::GenerateLocFilesImpl(const FString& CSVPaths, const FString& LocPath, const FString& LocName, const FString& GlobalNamespace, const FString& Separator, const FString& FallbackWhenEmpty, FString& OutMessage) +bool UELTEditor::GenerateLocFilesImpl(const FString& CSVPaths, const FString& LocPath, const FString& LocName, const FString& GlobalNamespace, const FString& Separator, const FString& FallbackWhenEmpty, bool bGenerateStringTables, FString& OutMessage) { - return GenerateLocFilesImpl(PathsStringToList(CSVPaths), LocPath, LocName, GlobalNamespace, Separator, FallbackWhenEmpty, OutMessage); + return GenerateLocFilesImpl(PathsStringToList(CSVPaths), LocPath, LocName, GlobalNamespace, Separator, FallbackWhenEmpty, bGenerateStringTables, OutMessage); } // Define the type of behavior when the localized string in CSV is empty and the fallback value should be used. @@ -481,7 +508,7 @@ enum class EFallbackWhenEmptyType : uint8 KEY }; -bool UELTEditor::GenerateLocFilesImpl(const TArray& CSVPaths, const FString& LocPath, const FString& LocName, const FString& GlobalNamespace, const FString& Separator, const FString& FallbackWhenEmpty, FString& OutMessage) +bool UELTEditor::GenerateLocFilesImpl(const TArray& CSVPaths, const FString& LocPath, const FString& LocName, const FString& GlobalNamespace, const FString& Separator, const FString& FallbackWhenEmpty, bool bGenerateStringTables, FString& OutMessage) { if (Separator.Len() != 1) { @@ -502,8 +529,14 @@ bool UELTEditor::GenerateLocFilesImpl(const TArray& CSVPaths, const FSt const FString MetaFileName = LocPath / LocName + TEXT(".locmeta"); const bool bLogDebug = UELTEditorSettings::GetLogDebug(); + bool bFirstCSV = true; TMap LocReses; + TMap> NamespaceToKeysMap; +#if ((ENGINE_MAJOR_VERSION == 5) && (ENGINE_MINOR_VERSION >= 8)) + TMap> NamespaceToKeysToNotesMap; +#endif + for (const FString& CSVPath : CSVPaths) { const FString CSVFilePath = FPaths::ConvertRelativePathToFull(CSVPath); @@ -514,28 +547,46 @@ bool UELTEditor::GenerateLocFilesImpl(const TArray& CSVPaths, const FSt FCSVReader Reader; if (Reader.LoadFromFile(CSVFilePath, (*Separator)[0], OutMessage)) { - int32 FirstColumn = 0; - bool bHasNamespaces = false; + int32 CurrentColumn = 0; + int32 FirstLangColumn = 0; + + int32 NamespaceColumn = INDEX_NONE; + int32 DevNotesColumn = INDEX_NONE; const TArray Columns = Reader.Columns; for (const FCSVColumn& Column : Columns) { if (Column.Values[0].Equals(TEXT("namespace"), ESearchCase::IgnoreCase)) { - bHasNamespaces=true; - break; + NamespaceColumn = CurrentColumn; + ++CurrentColumn; + ++FirstLangColumn; + continue; + } + if (Column.Values[0].Equals(TEXT("devnotes"), ESearchCase::IgnoreCase)) + { + DevNotesColumn = CurrentColumn; + ++CurrentColumn; + ++FirstLangColumn; + continue; } if (Column.Values[0].Equals(TEXT("key"), ESearchCase::IgnoreCase)) { + ++FirstLangColumn; break; } - ++FirstColumn; + if ((NamespaceColumn != INDEX_NONE) || (DevNotesColumn != INDEX_NONE)) + { + OutMessage = TEXT("ERROR: Invalid CSV! The 'namespace' and 'devnotes' columns must be before the 'key' column!"); + return false; + } + ++CurrentColumn; } - if (Columns.Num() > (FirstColumn + 1)) + if (Columns.Num() > (CurrentColumn + 1)) { - const int32 NumOfValues = Columns[FirstColumn].Values.Num(); - for (int32 CIdx = FirstColumn + 1; CIdx < Columns.Num(); CIdx++) + const int32 NumOfValues = Columns[CurrentColumn].Values.Num(); + for (int32 CIdx = CurrentColumn + 1; CIdx < Columns.Num(); CIdx++) { if (Columns[CIdx].Values.Num() != NumOfValues) { @@ -545,42 +596,81 @@ bool UELTEditor::GenerateLocFilesImpl(const TArray& CSVPaths, const FSt } // Potential place for namespaces. - const FCSVColumn& Namespaces = Columns[FirstColumn]; + const FCSVColumn& Namespaces = (NamespaceColumn != INDEX_NONE) ? Columns[NamespaceColumn] : Columns[0]; // Check if we have namespaces defined for every key or to use global value. - const bool bUseGlobalNamespace = (bHasNamespaces == false) && (GlobalNamespace.IsEmpty() == false); + const bool bUseGlobalNamespace = (NamespaceColumn == INDEX_NONE) && (GlobalNamespace.IsEmpty() == false); - if (bUseGlobalNamespace == false && bHasNamespaces == false) + if (bUseGlobalNamespace == false && (NamespaceColumn == INDEX_NONE)) { OutMessage = TEXT("ERROR: Namespaces in CSV not found!"); return false; } - // Clear the localization directory first. + // Potential place for devnotes. + const FCSVColumn& DevNotes = (DevNotesColumn != INDEX_NONE) ? Columns[DevNotesColumn] : Columns[0]; + + // Clear the localization directory first, preserving any .uasset files (e.g. string table assets). + // Deleting .uasset files while the corresponding UPackage is still in memory invalidates the async loader's package tracking and causes an assertion on the next reimport. if (bFirstCSV) { // Ensure we are not deleting any important files by checking if we are in Content directory and the Meta file is there exists. if (LocPath.Contains("Content") && IFileManager::Get().FileExists(*MetaFileName)) { - IFileManager::Get().DeleteDirectory(*LocPath, false, true); + TArray FilesToDelete; + IFileManager::Get().FindFilesRecursive(FilesToDelete, *LocPath, TEXT("*"), true, false); + for (const FString& File : FilesToDelete) + { + if (File.EndsWith(TEXT(".uasset")) == false) + { + IFileManager::Get().Delete(*File); + } + } } } - // Keys will be in first row if not having namespaces. - const FCSVColumn& Keys = Columns[bHasNamespaces ? FirstColumn+1 : FirstColumn]; + // Get the keys column and check if it is valid. + const FCSVColumn& Keys = Columns[FirstLangColumn-1]; if (Keys.Values[0].Equals(TEXT("key"), ESearchCase::IgnoreCase) == false) { OutMessage = TEXT("ERROR: Key column in CSV not found!"); return false; } +#if ((ENGINE_MAJOR_VERSION == 5) && (ENGINE_MINOR_VERSION >= 8)) + // Gather Dev Notes if available + if (DevNotesColumn != INDEX_NONE) + { + if (DevNotes.Values[0].Equals(TEXT("devnotes"), ESearchCase::IgnoreCase) == false) + { + OutMessage = TEXT("ERROR: Dev Notes column in CSV not found!"); + return false; + } + + for (int32 Key = 1; Key < Keys.Values.Num(); Key++) + { + FString DevNote = DevNotes.Values[Key]; + if (DevNote.IsEmpty() == false) + { + const FString& Namespace = (bUseGlobalNamespace || Namespaces.Values[Key].IsEmpty()) ? GlobalNamespace : Namespaces.Values[Key]; + if (Namespace.IsEmpty()) + { + OutMessage = FString::Printf(TEXT("ERROR: Namespace in row %i (counting from 1) for dev note is empty!"), Key); + return false; + } + TMap& DevNotesList = NamespaceToKeysToNotesMap.FindOrAdd(Namespace); + DevNotesList.Add(Keys.Values[Key], DevNote); + } + } + } +#endif + if (bLogDebug) { UE_LOG(ELTEditorLog, Log, TEXT("Adding Entries")); UE_LOG(ELTEditorLog, Log, TEXT("[Lang] | [Namespace] | [Key] | [Value]")); } - const int32 FirstLangColumn = bHasNamespaces ? (FirstColumn + 2) : (FirstColumn + 1); for (int32 Column = FirstLangColumn; Column < Columns.Num(); Column++) { const FCSVColumn& Locs = Columns[Column]; @@ -636,6 +726,11 @@ bool UELTEditor::GenerateLocFilesImpl(const TArray& CSVPaths, const FSt Keys.Values[Key], LocalizedString, 0); + + if (bGenerateStringTables && (Keys.Values[Key].IsEmpty() == false)) + { + NamespaceToKeysMap.FindOrAdd(Namespace).Add(Keys.Values[Key]); + } } } } @@ -674,6 +769,93 @@ bool UELTEditor::GenerateLocFilesImpl(const TArray& CSVPaths, const FSt LocRes.Value.SaveToFile(LocFileName); } + // Generate Key Reference String Table + if (bGenerateStringTables && NamespaceToKeysMap.Num() > 0) + { + FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked("AssetTools"); + + for (const auto& KVP : NamespaceToKeysMap) + { + const FString& Namespace = KVP.Key; + const TSet& Keys = KVP.Value; + + FString AssetName = FString::Printf(TEXT("ELT_KeyReferences_%s_%s"), *LocName, *Namespace); + FString PackagePath = FPackageName::FilenameToLongPackageName(LocPath / AssetName); + + // If the package is already in memory (e.g. from a previous reimport), use it directly. + UPackage* Package = FindPackage(nullptr, *PackagePath); + if (Package == nullptr) + { + if (FPackageName::DoesPackageExist(*PackagePath)) + { + Package = LoadPackage(nullptr, *PackagePath, LOAD_None); + } else + { + Package = CreatePackage(*PackagePath); + } + } + if (Package == nullptr) + { + OutMessage = FString::Printf(TEXT("ERROR: Failed to create package path for StringTable: %s"), *PackagePath); + return false; + } + + // Clear any existing StringTable from the package before creating a new one. + if (UStringTable* Existing = FindObject(Package, *AssetName)) + { + Existing->ClearFlags(RF_Public | RF_Standalone); + Existing->MarkAsGarbage(); + } + + UStringTable* StringTableAsset = NewObject(Package, UStringTable::StaticClass(), FName(*AssetName), (RF_Public | RF_Standalone | RF_Transactional)); + if (StringTableAsset == nullptr) + { + OutMessage = FString::Printf(TEXT("ERROR: Failed to create StringTable asset: %s"), *AssetName); + return false; + } + + FAssetRegistryModule::AssetCreated(StringTableAsset); + Package->MarkPackageDirty(); + + FStringTableRef StringTableRef = StringTableAsset->GetMutableStringTable(); + StringTableRef->SetNamespace(Namespace); + +#if ((ENGINE_MAJOR_VERSION == 5) && (ENGINE_MINOR_VERSION >= 8)) + TMap* KeysToNotes = NamespaceToKeysToNotesMap.Find(Namespace); + for (const FString& Key : Keys) + { + FString DevNotes = TEXT(""); + if (FString* Note = KeysToNotes ? KeysToNotes->Find(Key) : nullptr) + { + DevNotes = *Note; + } + + StringTableRef->SetSourceString(FTextKey(Key), Key, DevNotes); + } +#else + for (const FString& Key : Keys) + { + StringTableRef->SetSourceString(FTextKey(Key), Key); + } +#endif + + FString PackageFileName = FPackageName::LongPackageNameToFilename(PackagePath, FPackageName::GetAssetPackageExtension()); + FSavePackageArgs SaveArgs; + SaveArgs.TopLevelFlags = RF_Public | RF_Standalone; + SaveArgs.Error = GError; + if (UPackage::SavePackage(Package, StringTableAsset, *PackageFileName, SaveArgs) == false) + { + OutMessage = FString::Printf(TEXT("ERROR: Failed to save StringTable package file to disk path: %s"), *PackageFileName); + return false; + } + + if (bLogDebug) + { + UE_LOG(ELTEditorLog, Log, TEXT("Saved String Table Asset: %s"), *PackageFileName); + } + } + } + OutMessage = TEXT("SUCCESS: Localization import complete!"); return true; } diff --git a/Source/EasyLocalizationToolEditor/Private/ELTEditorAuditWidget.cpp b/Source/EasyLocalizationToolEditor/Private/ELTEditorAuditWidget.cpp new file mode 100644 index 0000000..009e3bd --- /dev/null +++ b/Source/EasyLocalizationToolEditor/Private/ELTEditorAuditWidget.cpp @@ -0,0 +1,919 @@ +// Copyright (c) 2026 Crezetique. All rights reserved. + +#include "ELTEditorAuditWidget.h" +#include "ELTEditorAuditor.h" +#include "ELTSettings.h" + +#include "Framework/Docking/TabManager.h" +#include "Widgets/Docking/SDockTab.h" +#include "Widgets/Layout/SBorder.h" +#include "Widgets/Layout/SBox.h" +#include "Widgets/Text/STextBlock.h" +#include "Widgets/Input/SButton.h" +#include "Widgets/Input/SCheckBox.h" +#include "Widgets/Input/SComboBox.h" +#include "Widgets/Input/SHyperlink.h" +#include "Widgets/Views/SHeaderRow.h" +#include "Widgets/Views/STableRow.h" +#include "Widgets/SBoxPanel.h" + +#include "Engine/Blueprint.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "Kismet2/KismetEditorUtilities.h" +#include "Subsystems/AssetEditorSubsystem.h" +#include "Internationalization/Internationalization.h" +#include "Internationalization/Culture.h" +#include "HAL/PlatformApplicationMisc.h" +#include "Misc/MessageDialog.h" +#include "Editor.h" + +#if (ENGINE_MAJOR_VERSION >= 5) + #define ELT_APP_STYLE FAppStyle::Get() + #define ELT_GET_BRUSH(Name) FAppStyle::GetBrush(Name) +#else + #include "EditorStyleSet.h" + #define ELT_APP_STYLE FEditorStyle::Get() + #define ELT_GET_BRUSH(Name) FEditorStyle::GetBrush(Name) +#endif + +DEFINE_LOG_CATEGORY_STATIC(ELTAuditWidgetLog, Log, All); + +namespace ELTAuditCol +{ + static const FName AssetName = TEXT("AssetName"); + static const FName Type = TEXT("Type"); + static const FName VariableNode = TEXT("VariableNode"); + static const FName Issue = TEXT("Issue"); + static const FName QuickAction = TEXT("QuickAction"); + static const FName LocalizedString = TEXT("LocalizedString"); + static const FName Value = TEXT("Value"); + static const FName Key = TEXT("Key"); + static const FName Namespace = TEXT("Namespace"); + static const FName IsUsingStringTable = TEXT("IsUsingStringTable"); +} + +namespace ELTAuditColor +{ + // Cell text colours + static const FLinearColor Default = FLinearColor::White; + static const FLinearColor Muted = FLinearColor(0.5f, 0.5f, 0.5f); + + // Type column + static const FLinearColor TypeClassVariable = FLinearColor(.7f, 0.4f, 0.9f); + static const FLinearColor TypeFunctionVariable = FLinearColor(0.4f, 0.7f, 1.f); + static const FLinearColor TypeNodeParameter = FLinearColor(1.f, 0.8f, 0.2f); + static const FLinearColor TypeWidgetComponent = FLinearColor(0.4f, 0.9f, 0.4f); + + // Issue column + static const FLinearColor IssueNone = FLinearColor(0.5f, 0.5f, 0.5f); + static const FLinearColor IssueWarning = FLinearColor(1.f, 0.8f, 0.2f); + static const FLinearColor IssueError = FLinearColor(1.f, 0.4f, 0.4f); + + // IsUsingStringTable column + static const FLinearColor StringTableTrue = FLinearColor(0.4f, 0.9f, 0.4f); + static const FLinearColor StringTableFalse = FLinearColor(1.f, 0.4f, 0.4f); + + // Primary column background tint + static const FLinearColor PrimaryColumnTint = FLinearColor(1.f, 1.f, 1.f, 0.05f); + + // Tip text + static const FLinearColor TipText = FLinearColor::White; +} + +const FName UELTEditorAuditWidget::TabName = TEXT("ELTLocalizationAudit"); +TArray UELTEditorAuditWidget::PendingResults; +TWeakPtr UELTEditorAuditWidget::LiveWidget; +FOnAuditWidgetReimportCSV UELTEditorAuditWidget::OnReimportCSVDelegate; +bool UELTEditorAuditWidget::bPendingDialogOnSpawn = false; + +/*static*/ void UELTEditorAuditWidget::ShowCompletionDialog(const TArray& Results) +{ + int32 IssueCount = 0; + TSet AffectedAssets; + for (const FELTAssetAuditResult& Result : Results) + { + for (const FELTAuditIssue& Issue : Result.Issues) + { + if (Issue.HasIssue()) + { + ++IssueCount; + AffectedAssets.Add(Issue.AssetName); + } + } + } + + const FString Summary = (IssueCount == 0) + ? FString::Printf(TEXT("All %d asset(s) passed localization audit."), Results.Num()) + : FString::Printf(TEXT("%d issue(s) found across %d / %d asset(s)."), IssueCount, AffectedAssets.Num(), Results.Num()); + +#if (ENGINE_MAJOR_VERSION == 5) + const EAppMsgCategory Category = (IssueCount == 0) ? EAppMsgCategory::Success : EAppMsgCategory::Warning; + FMessageDialog::Open(Category, EAppMsgType::Ok, FText::FromString(Summary), FText::FromString(TEXT("Audit Complete"))); +#else + FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(Summary)); +#endif +} + +/*static*/ void UELTEditorAuditWidget::ShowResults(const TArray& Results) +{ + PendingResults = Results; + + FGlobalTabmanager& TM = *FGlobalTabmanager::Get(); + + if (TM.HasTabSpawner(TabName) == false) + { + FTabSpawnerEntry& Entry = TM.RegisterNomadTabSpawner( + TabName, FOnSpawnTab::CreateStatic(&UELTEditorAuditWidget::SpawnTab)); + + Entry + .SetDisplayName(FText::FromString(TEXT("Localization Audit"))) + .SetTooltipText(FText::FromString(TEXT("Shows FText localization issues found in selected assets."))) + .SetMenuType(ETabSpawnerMenuType::Hidden); + } + + if (TSharedPtr Pinned = LiveWidget.Pin()) + { + Pinned->SetPendingCompletionDialog(true); + Pinned->Refresh(Results); + TM.TryInvokeTab(TabName); + return; + } + + // Mark that a completion dialog should show when the widget is constructed. + bPendingDialogOnSpawn = true; + TM.TryInvokeTab(TabName); +} + +/*static*/ void UELTEditorAuditWidget::UpdateAvailableLanguages(const TArray& Languages) +{ + if (TSharedPtr Pinned = LiveWidget.Pin()) + { + Pinned->RefreshLanguages(Languages); + } +} + +/*static*/ TSharedRef UELTEditorAuditWidget::SpawnTab(const FSpawnTabArgs& Args) +{ + TSharedPtr Widget; + + TSharedRef Tab = SNew(SDockTab) + .TabRole(ETabRole::NomadTab) + .Label(FText::FromString(TEXT("Localization Audit"))) + [ + SAssignNew(Widget, SELTEditorAuditWidget) + .AuditResults(PendingResults) + ]; + + LiveWidget = Widget; + return Tab; +} + +class SELTAuditIssueRow : public SMultiColumnTableRow> +{ +public: + SLATE_BEGIN_ARGS(SELTAuditIssueRow) {} + SLATE_ARGUMENT(TSharedPtr, Issue) + SLATE_ARGUMENT(FAssetData,AssetData) + SLATE_END_ARGS() + + void Construct(const FArguments& InArgs, const TSharedRef& OwnerTable) + { + Issue = InArgs._Issue; + AssetData = InArgs._AssetData; + + // Initialised here so it has the same lifetime as the row widget — safe to take &. + CopyButtonStyle = FCoreStyle::Get().GetWidgetStyle("NoBorder"); + CopyButtonStyle.Normal = FSlateNoResource(); + CopyButtonStyle.Hovered = FSlateNoResource(); + CopyButtonStyle.Pressed = FSlateNoResource(); + + SMultiColumnTableRow>::Construct( + SMultiColumnTableRow::FArguments().Padding(FMargin(2.f, 1.f)), OwnerTable); + } + + virtual TSharedRef GenerateWidgetForColumn(const FName& ColumnId) override + { + if (Issue.IsValid() == false) + { + return SNullWidget::NullWidget; + } + + const FELTAuditIssue& Row = *Issue; + + // Detail columns have no background tint; primary columns (first five) get a subtle lighter shade. + auto Cell = [this](const FString& Str, FLinearColor Col = ELTAuditColor::Default) -> TSharedRef + { + return SNew(SButton) + .ButtonStyle(&CopyButtonStyle) + .ContentPadding(FMargin(6.f, 2.f)) + .Cursor(EMouseCursor::Hand) + .ToolTipText(FText::FromString(TEXT("Left-click to copy"))) + .OnClicked_Lambda([Str]() -> FReply + { + FPlatformApplicationMisc::ClipboardCopy(*Str); + return FReply::Handled(); + }) + [ + SNew(STextBlock) + .Text(FText::FromString(Str)) + .ColorAndOpacity(Col) + ]; + }; + + auto PrimaryCell = [this](const FString& Str, FLinearColor Col = ELTAuditColor::Default) -> TSharedRef + { + return SNew(SBorder) + .BorderImage(ELT_GET_BRUSH("WhiteTexture")) + .BorderBackgroundColor(ELTAuditColor::PrimaryColumnTint) + .Padding(0.f) + [ + SNew(SButton) + .ButtonStyle(&CopyButtonStyle) + .ContentPadding(FMargin(6.f, 2.f)) + .Cursor(EMouseCursor::Hand) + .ToolTipText(FText::FromString(TEXT("Left-click to copy"))) + .OnClicked_Lambda([Str]() -> FReply + { + FPlatformApplicationMisc::ClipboardCopy(*Str); + return FReply::Handled(); + }) + [ + SNew(STextBlock) + .Text(FText::FromString(Str)) + .ColorAndOpacity(Col) + ] + ]; + }; + + if (ColumnId == ELTAuditCol::AssetName) + { + return PrimaryCell(Row.AssetName, ELTAuditColor::Default); + } + + if (ColumnId == ELTAuditCol::QuickAction) + { + const FAssetData CapturedAsset = AssetData; + const FELTAuditIssue CapturedRow = Row; + + FString ActionLabel; + FString ActionTooltip; + + switch (Row.Type) + { + case EELTAuditParameterType::ClassVariable: + ActionLabel = TEXT("Open Asset"); + ActionTooltip = TEXT("Open this asset in its default editor"); + break; + case EELTAuditParameterType::FunctionVariable: + ActionLabel = TEXT("Jump to Function"); + ActionTooltip = FString::Printf(TEXT("Navigate to function '%s'"), *Row.GraphName); + break; + case EELTAuditParameterType::NodeParameter: + ActionLabel = TEXT("Jump to Node"); + ActionTooltip = FString::Printf(TEXT("Focus the node in graph '%s'"), *Row.GraphName); + break; + case EELTAuditParameterType::WidgetComponent: + ActionLabel = TEXT("Open Asset"); + ActionTooltip = TEXT("Open this Widget Blueprint in the designer"); + break; + } + + return SNew(SBorder) + .BorderImage(ELT_GET_BRUSH("WhiteTexture")) + .BorderBackgroundColor(ELTAuditColor::PrimaryColumnTint) + .Padding(FMargin(6.f, 1.f)) + [ + SNew(SHyperlink) + .Text(FText::FromString(ActionLabel)) + .ToolTipText(FText::FromString(ActionTooltip)) + .OnNavigate_Lambda([CapturedAsset, CapturedRow]() + { + SELTEditorAuditWidget::JumpToIssue(CapturedAsset, CapturedRow); + }) + ]; + } + + if (ColumnId == ELTAuditCol::Type) + { + FLinearColor TypeCol = ELTAuditColor::TypeClassVariable; + if (Row.Type == EELTAuditParameterType::FunctionVariable) { TypeCol = ELTAuditColor::TypeFunctionVariable; } + else if (Row.Type == EELTAuditParameterType::NodeParameter) { TypeCol = ELTAuditColor::TypeNodeParameter; } + else if (Row.Type == EELTAuditParameterType::WidgetComponent) { TypeCol = ELTAuditColor::TypeWidgetComponent; } + return PrimaryCell(Row.TypeDisplayString(), TypeCol); + } + + if (ColumnId == ELTAuditCol::VariableNode) { return PrimaryCell(Row.VariableNodeName, ELTAuditColor::Default); } + + if (ColumnId == ELTAuditCol::Issue) + { + FLinearColor IssueCol; + if (Row.Issue == EELTAuditIssueType::None) { IssueCol = ELTAuditColor::IssueNone; } + else if (Row.Issue == EELTAuditIssueType::EmptyValue) { IssueCol = ELTAuditColor::IssueWarning; } + else { IssueCol = ELTAuditColor::IssueError; } + + return SNew(SBorder) + .BorderImage(ELT_GET_BRUSH("WhiteTexture")) + .BorderBackgroundColor(ELTAuditColor::PrimaryColumnTint) + .Padding(FMargin(6.f, 2.f)) + [ + SNew(STextBlock) + .Text(FText::FromString(Row.IssueDisplayString())) + .ColorAndOpacity(IssueCol) + .ToolTipText(FText::FromString(Row.IssueTooltipString())) + ]; + } + + if (ColumnId == ELTAuditCol::LocalizedString) { return Cell(Row.LocalizedString, ELTAuditColor::Muted); } + if (ColumnId == ELTAuditCol::Value) { return Cell(Row.Value, ELTAuditColor::Muted); } + if (ColumnId == ELTAuditCol::Key) { return Cell(Row.Key, ELTAuditColor::Muted); } + if (ColumnId == ELTAuditCol::Namespace) { return Cell(Row.Namespace, ELTAuditColor::Muted); } + + if (ColumnId == ELTAuditCol::IsUsingStringTable) + { + const bool bUsing = Row.bIsUsingStringTable; + return Cell(bUsing ? TEXT("true") : TEXT("false"), + bUsing ? ELTAuditColor::StringTableTrue : ELTAuditColor::StringTableFalse); + } + + return SNullWidget::NullWidget; + } + +private: + TSharedPtr Issue; + FAssetData AssetData; + FButtonStyle CopyButtonStyle; +}; + +void SELTEditorAuditWidget::RebuildAuditData(const TArray& InResults) +{ + AllIssues.Reset(); + FlatIssues.Reset(); + AssetDataMap.Reset(); + LastAuditedAssets.Reset(); + + for (const FELTAssetAuditResult& Result : InResults) + { + AssetDataMap.Emplace(Result.AssetData.AssetName.ToString(), Result.AssetData); + LastAuditedAssets.Add(Result.AssetData); + + for (const FELTAuditIssue& Issue : Result.Issues) + { + AllIssues.Add(MakeShared(Issue)); + } + } +} + +FText SELTEditorAuditWidget::BuildSummaryText() const +{ + const int32 TotalAssets = AssetDataMap.Num(); + + int32 IssueCount = 0; + TSet AffectedAssets; + for (const FIssuePtr& Ptr : AllIssues) + { + if (Ptr.IsValid() && Ptr->HasIssue()) + { + ++IssueCount; + AffectedAssets.Add(Ptr->AssetName); + } + } + + if (IssueCount == 0) + { + return FText::FromString(FString::Printf(TEXT("✔ All %d asset(s) passed localization audit."), TotalAssets)); + } + + return FText::FromString(FString::Printf(TEXT("⚠ %d issue(s) across %d / %d asset(s)."), IssueCount, AffectedAssets.Num(), TotalAssets)); +} + +EColumnSortMode::Type SELTEditorAuditWidget::GetSortModeForColumn(FName Column) const +{ + return (SortColumn == Column) ? SortMode : EColumnSortMode::None; +} + +void SELTEditorAuditWidget::OnSortColumn(EColumnSortPriority::Type /*Priority*/, const FName& Column, EColumnSortMode::Type Mode) +{ + SortColumn = Column; + SortMode = Mode; + ApplySort(); + + if (ListView.IsValid()) { ListView->RequestListRefresh(); } +} + +void SELTEditorAuditWidget::ApplySort() +{ + if (SortMode == EColumnSortMode::None || SortColumn.IsNone()) + { + return; + } + + auto GetSortKey = [](const FIssuePtr& Ptr, const FName& Col) -> FString + { + if (Ptr.IsValid() == false) { return TEXT(""); } + const FELTAuditIssue& I = *Ptr; + if (Col == ELTAuditCol::AssetName) { return I.AssetName; } + if (Col == ELTAuditCol::Type) { return I.TypeDisplayString(); } + if (Col == ELTAuditCol::VariableNode) { return I.VariableNodeName; } + if (Col == ELTAuditCol::Issue) { return I.IssueDisplayString(); } + if (Col == ELTAuditCol::LocalizedString) { return I.LocalizedString; } + if (Col == ELTAuditCol::Value) { return I.Value; } + if (Col == ELTAuditCol::Key) { return I.Key; } + if (Col == ELTAuditCol::Namespace) { return I.Namespace; } + if (Col == ELTAuditCol::IsUsingStringTable) { return I.bIsUsingStringTable ? TEXT("true") : TEXT("false"); } + return TEXT(""); + }; + + const FName Col = SortColumn; + const bool bAscend = (SortMode == EColumnSortMode::Ascending); + + FlatIssues.Sort([&](const FIssuePtr& A, const FIssuePtr& B) + { + const int32 Cmp = GetSortKey(A, Col).Compare(GetSortKey(B, Col), ESearchCase::IgnoreCase); + return bAscend ? Cmp < 0 : Cmp > 0; + }); +} + +void SELTEditorAuditWidget::ApplyFilter() +{ + FlatIssues.Reset(); + for (const FIssuePtr& Ptr : AllIssues) + { + if (Ptr.IsValid() == false) { continue; } + + if (Ptr->Issue == EELTAuditIssueType::None) + { + if (bFilterIssues == false) { FlatIssues.Add(Ptr); } + } + else if (Ptr->Issue == EELTAuditIssueType::EmptyValue) + { + if (bHideEmpty == false) { FlatIssues.Add(Ptr); } + } + else + { + FlatIssues.Add(Ptr); + } + } +} + +void SELTEditorAuditWidget::OnFilterIssuesChanged(ECheckBoxState NewState) +{ + bFilterIssues = (NewState == ECheckBoxState::Checked); + ApplyFilter(); + ApplySort(); + if (ListView.IsValid()) { ListView->RequestListRefresh(); } + if (SummaryText.IsValid()) { SummaryText->SetText(BuildSummaryText()); } +} + +void SELTEditorAuditWidget::OnHideEmptyChanged(ECheckBoxState NewState) +{ + bHideEmpty = (NewState == ECheckBoxState::Checked); + ApplyFilter(); + ApplySort(); + if (ListView.IsValid()) { ListView->RequestListRefresh(); } + if (SummaryText.IsValid()) { SummaryText->SetText(BuildSummaryText()); } +} + +void SELTEditorAuditWidget::OnLanguageSelected(FLanguagePtr Item, ESelectInfo::Type SelectInfo) +{ + if (Item.IsValid() == false) { return; } + + SelectedLanguage = Item; + FInternationalization::Get().SetCurrentLanguage(*Item); + + // Reaudit the same assets so LocalizedString values reflect the new language. + const TArray Results = UELTEditorAuditor::RunAudit(LastAuditedAssets); + Refresh(Results); +} + +TSharedRef SELTEditorAuditWidget::OnGenerateLanguageComboRow(FLanguagePtr Item) +{ + return SNew(STextBlock) + .Text(FText::FromString(Item.IsValid() ? *Item : TEXT(""))) + .Margin(FMargin(4.f, 2.f)); +} + +void SELTEditorAuditWidget::ApplyDetailColumnVisibility() +{ + if (AuditHeaderRow.IsValid() == false) { return; } + +#if (ENGINE_MAJOR_VERSION >= 5) + for (const FName& Col : GetDetailColumns()) + { + AuditHeaderRow->SetShowGeneratedColumn(Col, bShowMoreDetails); + } +#endif + // SetShowGeneratedColumn is not available in UE4 — detail columns are always visible there. +} + +void SELTEditorAuditWidget::OnShowMoreDetailsChanged(ECheckBoxState NewState) +{ + bShowMoreDetails = (NewState == ECheckBoxState::Checked); + ApplyDetailColumnVisibility(); +} + +FReply SELTEditorAuditWidget::OnRefreshAuditClicked() +{ + const TArray Results = UELTEditorAuditor::RunAudit(LastAuditedAssets); + bPendingCompletionDialog = true; + Refresh(Results); + return FReply::Handled(); +} + +FReply SELTEditorAuditWidget::OnReimportCSVClicked() +{ + // GenerateLocFilesImpl calls LoadPackage synchronously. If async loading is already in process, subsequent loads trigger an assertion. + if (IsAsyncLoading()) + { + UE_LOG(ELTAuditWidgetLog, Warning, TEXT("Reimport CSV skipped — async loading is in progress. Please wait a moment and try again.")); + return FReply::Handled(); + } + + UELTEditorAuditWidget::OnReimportCSVDelegate.ExecuteIfBound(); + return FReply::Handled(); +} + +FText SELTEditorAuditWidget::PickRandomTip() const +{ + static const TArray Tips = { + TEXT("Copy to Clipboard by Left-Clicking audit table cells. Useful for copying Values to input on a separate localization sheet."), + TEXT("Sort the audit table content by clicking the column headers."), + }; + + const int32 Index = FMath::RandRange(0, Tips.Num() - 1); + return FText::FromString(FString::Printf(TEXT("Tip: %s"), *Tips[Index])); +} + +void SELTEditorAuditWidget::Construct(const FArguments& InArgs) +{ + RebuildAuditData(InArgs._AuditResults); + + const TArray Langs = UELTSettings::GetAvailableLanguages(); + for (const FString& Lang : Langs) + { + AvailableLanguages.Add(MakeShared(Lang)); + } + + // Default current language, fallback to the first available. + const FString CurrentLang = FInternationalization::Get().GetCurrentLanguage()->GetName(); + for (const FLanguagePtr& LangPtr : AvailableLanguages) + { + if (LangPtr.IsValid() && *LangPtr == CurrentLang) + { + SelectedLanguage = LangPtr; + break; + } + } + if (SelectedLanguage.IsValid() == false && AvailableLanguages.Num() > 0) + { + SelectedLanguage = AvailableLanguages[0]; + } + + // Set sort state before building the header so the attribute delegate returns + SortColumn = ELTAuditCol::AssetName; + SortMode = EColumnSortMode::Ascending; + + SAssignNew(AuditHeaderRow, SHeaderRow) + + + SHeaderRow::Column(ELTAuditCol::AssetName) + .DefaultLabel(FText::FromString(TEXT("Asset Name"))) + .FillWidth(0.11f) + .SortMode(this, &SELTEditorAuditWidget::GetSortModeForColumn, ELTAuditCol::AssetName) + .OnSort(this, &SELTEditorAuditWidget::OnSortColumn) + + + SHeaderRow::Column(ELTAuditCol::Type) + .DefaultLabel(FText::FromString(TEXT("Type"))) + .FillWidth(0.10f) + .SortMode(this, &SELTEditorAuditWidget::GetSortModeForColumn, ELTAuditCol::Type) + .OnSort(this, &SELTEditorAuditWidget::OnSortColumn) + + + SHeaderRow::Column(ELTAuditCol::VariableNode) + .DefaultLabel(FText::FromString(TEXT("Variable/Node Name"))) + .FillWidth(0.13f) + .SortMode(this, &SELTEditorAuditWidget::GetSortModeForColumn, ELTAuditCol::VariableNode) + .OnSort(this, &SELTEditorAuditWidget::OnSortColumn) + + + SHeaderRow::Column(ELTAuditCol::Issue) + .DefaultLabel(FText::FromString(TEXT("Issue"))) + .FillWidth(0.12f) + .SortMode(this, &SELTEditorAuditWidget::GetSortModeForColumn, ELTAuditCol::Issue) + .OnSort(this, &SELTEditorAuditWidget::OnSortColumn) + + + SHeaderRow::Column(ELTAuditCol::QuickAction) + .DefaultLabel(FText::FromString(TEXT("Quick Action"))) + .FixedWidth(110.f) + + + SHeaderRow::Column(ELTAuditCol::LocalizedString) + .DefaultLabel(FText::FromString(TEXT("Localized String"))) + .FillWidth(0.13f) + .SortMode(this, &SELTEditorAuditWidget::GetSortModeForColumn, ELTAuditCol::LocalizedString) + .OnSort(this, &SELTEditorAuditWidget::OnSortColumn) + + + SHeaderRow::Column(ELTAuditCol::Value) + .DefaultLabel(FText::FromString(TEXT("Value"))) + .FillWidth(0.13f) + .SortMode(this, &SELTEditorAuditWidget::GetSortModeForColumn, ELTAuditCol::Value) + .OnSort(this, &SELTEditorAuditWidget::OnSortColumn) + + + SHeaderRow::Column(ELTAuditCol::Key) + .DefaultLabel(FText::FromString(TEXT("Key"))) + .FillWidth(0.09f) + .SortMode(this, &SELTEditorAuditWidget::GetSortModeForColumn, ELTAuditCol::Key) + .OnSort(this, &SELTEditorAuditWidget::OnSortColumn) + + + SHeaderRow::Column(ELTAuditCol::Namespace) + .DefaultLabel(FText::FromString(TEXT("Namespace"))) + .FillWidth(0.10f) + .SortMode(this, &SELTEditorAuditWidget::GetSortModeForColumn, ELTAuditCol::Namespace) + .OnSort(this, &SELTEditorAuditWidget::OnSortColumn) + + + SHeaderRow::Column(ELTAuditCol::IsUsingStringTable) + .DefaultLabel(FText::FromString(TEXT("Is Using String Table"))) + .FillWidth(0.09f) + .SortMode(this, &SELTEditorAuditWidget::GetSortModeForColumn, ELTAuditCol::IsUsingStringTable) + .OnSort(this, &SELTEditorAuditWidget::OnSortColumn); + + SAssignNew(ListView, SListView) + .ListItemsSource(&FlatIssues) + .OnGenerateRow(this, &SELTEditorAuditWidget::OnGenerateIssueRow) + .HeaderRow(AuditHeaderRow.ToSharedRef()) + .SelectionMode(ESelectionMode::Single); + + ApplyFilter(); + ApplySort(); + ApplyDetailColumnVisibility(); + + ChildSlot + [ + SNew(SVerticalBox) + + + SVerticalBox::Slot() + .AutoHeight() + .Padding(8.f, 8.f, 8.f, 4.f) + [ + SNew(SHorizontalBox) + + + SHorizontalBox::Slot() + .VAlign(VAlign_Center) + .FillWidth(1.f) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Localization Audit"))) + .TextStyle(ELT_APP_STYLE, "LargeText") + ] + + + SHorizontalBox::Slot() + .VAlign(VAlign_Center) + .AutoWidth() + .Padding(0.f, 0.f, 12.f, 0.f) + [ + SNew(SCheckBox) + .IsChecked(ECheckBoxState::Unchecked) + .OnCheckStateChanged(this, &SELTEditorAuditWidget::OnFilterIssuesChanged) + .ToolTipText(FText::FromString(TEXT("When checked, FText entries that passed the audit are hidden"))) + [ + SNew(STextBlock).Text(FText::FromString(TEXT("Hide Valid Localization"))) + ] + ] + + + SHorizontalBox::Slot() + .VAlign(VAlign_Center) + .AutoWidth() + .Padding(0.f, 0.f, 12.f, 0.f) + [ + SNew(SCheckBox) + .IsChecked(ECheckBoxState::Unchecked) + .OnCheckStateChanged(this, &SELTEditorAuditWidget::OnHideEmptyChanged) + .ToolTipText(FText::FromString(TEXT("When checked, FText fields with no value set are hidden"))) + [ + SNew(STextBlock).Text(FText::FromString(TEXT("Hide Empty"))) + ] + ] + + + SHorizontalBox::Slot() + .VAlign(VAlign_Center) + .AutoWidth() + .Padding(0.f, 0.f, 12.f, 0.f) + [ + SNew(SCheckBox) + .IsChecked(ECheckBoxState::Checked) + .OnCheckStateChanged(this, &SELTEditorAuditWidget::OnShowMoreDetailsChanged) + .ToolTipText(FText::FromString(TEXT("When checked, additional detail columns are shown (Localized String, Value, Key, Namespace, Is Using String Table)"))) + [ + SNew(STextBlock).Text(FText::FromString(TEXT("Show More Details"))) + ] + ] + + + SHorizontalBox::Slot() + .VAlign(VAlign_Center) + .AutoWidth() + .Padding(0.f, 0.f, 8.f, 0.f) + [ + SAssignNew(LanguageComboBox, SComboBox) + .OptionsSource(&AvailableLanguages) + .InitiallySelectedItem(SelectedLanguage) + .OnSelectionChanged(this, &SELTEditorAuditWidget::OnLanguageSelected) + .OnGenerateWidget(this, &SELTEditorAuditWidget::OnGenerateLanguageComboRow) + .ToolTipText(FText::FromString(TEXT("Preview language for Localized String column"))) + [ + SNew(STextBlock) + .Text_Lambda([this]() + { + return FText::FromString(SelectedLanguage.IsValid() ? *SelectedLanguage : TEXT("")); + }) + ] + ] + + + SHorizontalBox::Slot() + .VAlign(VAlign_Center) + .AutoWidth() + .Padding(0.f, 0.f, 8.f, 0.f) + [ + SNew(SButton) + .Text(FText::FromString(TEXT("Reaudit Assets"))) + .ToolTipText(FText::FromString(TEXT("Re-run the audit on all previously audited assets"))) + .OnClicked(this, &SELTEditorAuditWidget::OnRefreshAuditClicked) + ] + + + SHorizontalBox::Slot() + .VAlign(VAlign_Center) + .AutoWidth() + [ + SNew(SButton) + .Text(FText::FromString(TEXT("Reimport CSV"))) + .ToolTipText(FText::FromString(TEXT("Reimport CSVs defined within the Easy Localization Tool."))) + .OnClicked(this, &SELTEditorAuditWidget::OnReimportCSVClicked) + ] + ] + + + SVerticalBox::Slot() + .AutoHeight() + .Padding(8.f, 0.f, 8.f, 6.f) + [ + SNew(SBorder) + .BorderImage(ELT_GET_BRUSH("ToolPanel.GroupBorder")) + .Padding(FMargin(8.f, 4.f)) + [ + SAssignNew(SummaryText, STextBlock) + .Text(BuildSummaryText()) + .TextStyle(ELT_APP_STYLE, "NormalText.Important") + ] + ] + + + SVerticalBox::Slot() + .FillHeight(1.f) + .Padding(8.f, 0.f, 8.f, 4.f) + [ + SNew(SBorder) + .BorderImage(ELT_GET_BRUSH("ToolPanel.GroupBorder")) + [ + ListView.ToSharedRef() + ] + ] + + + SVerticalBox::Slot() + .AutoHeight() + .Padding(8.f, 0.f, 8.f, 8.f) + [ + SAssignNew(TipText, STextBlock) + .Text(PickRandomTip()) + .ColorAndOpacity(ELTAuditColor::TipText) + ] + ]; + + // Fire the completion dialog now that the widget is fully constructed. + if (UELTEditorAuditWidget::bPendingDialogOnSpawn) + { + UELTEditorAuditWidget::bPendingDialogOnSpawn = false; + UELTEditorAuditWidget::ShowCompletionDialog(InArgs._AuditResults); + } +} + +void SELTEditorAuditWidget::Refresh(const TArray& InResults) +{ + RebuildAuditData(InResults); + ApplyFilter(); + ApplySort(); + + if (ListView.IsValid()) { ListView->RequestListRefresh(); } + if (SummaryText.IsValid()) { SummaryText->SetText(BuildSummaryText()); } + if (TipText.IsValid()) { TipText->SetText(PickRandomTip()); } + + if (bPendingCompletionDialog) + { + bPendingCompletionDialog = false; + UELTEditorAuditWidget::ShowCompletionDialog(InResults); + } +} + +void SELTEditorAuditWidget::RefreshLanguages(const TArray& Languages) +{ + AvailableLanguages.Reset(); + for (const FString& Lang : Languages) + { + AvailableLanguages.Add(MakeShared(Lang)); + } + + // Preserve the current selection if it still exists in the new list, otherwise fall back to the first entry. + bool bSelectionStillValid = false; + if (SelectedLanguage.IsValid()) + { + for (const FLanguagePtr& LangPtr : AvailableLanguages) + { + if (LangPtr.IsValid() && *LangPtr == *SelectedLanguage) + { + SelectedLanguage = LangPtr; + bSelectionStillValid = true; + break; + } + } + } + + if (bSelectionStillValid == false && AvailableLanguages.Num() > 0) + { + SelectedLanguage = AvailableLanguages[0]; + } + + if (LanguageComboBox.IsValid()) + { + LanguageComboBox->RefreshOptions(); + LanguageComboBox->SetSelectedItem(SelectedLanguage); + } +} + +TSharedRef SELTEditorAuditWidget::OnGenerateIssueRow( + FIssuePtr Issue, + const TSharedRef& OwnerTable) +{ + FAssetData RowAsset; + if (Issue.IsValid()) + { + if (const FAssetData* Found = AssetDataMap.Find(Issue->AssetName)) + { + RowAsset = *Found; + } + } + + return SNew(SELTAuditIssueRow, OwnerTable) + .Issue(Issue) + .AssetData(RowAsset); +} + +/*static*/ void SELTEditorAuditWidget::JumpToIssue( + const FAssetData& AssetData, + const FELTAuditIssue& Issue) +{ + UObject* Asset = AssetData.GetAsset(); + if (Asset == nullptr) { return; } + + UAssetEditorSubsystem* AssetEditorSS = GEditor->GetEditorSubsystem(); + + if (Issue.Type == EELTAuditParameterType::ClassVariable || + Issue.Type == EELTAuditParameterType::WidgetComponent) + { + AssetEditorSS->OpenEditorForAsset(Asset); + return; + } + + UBlueprint* Blueprint = Cast(Asset); + if (Blueprint == nullptr) + { + AssetEditorSS->OpenEditorForAsset(Asset); + return; + } + + if (Issue.Type == EELTAuditParameterType::FunctionVariable && Issue.GraphName.IsEmpty() == false) + { + for (UEdGraph* Graph : Blueprint->FunctionGraphs) + { + if (Graph != nullptr && Graph->GetName() == Issue.GraphName) + { + FKismetEditorUtilities::BringKismetToFocusAttentionOnObject(Graph); + return; + } + } + FKismetEditorUtilities::BringKismetToFocusAttentionOnObject(Blueprint); + return; + } + + if (Issue.Type == EELTAuditParameterType::NodeParameter && Issue.NodeGuid.IsValid()) + { + TArray AllGraphs; + Blueprint->GetAllGraphs(AllGraphs); + + for (UEdGraph* Graph : AllGraphs) + { + if (Graph == nullptr) { continue; } + for (UEdGraphNode* Node : Graph->Nodes) + { + if (Node != nullptr && Node->NodeGuid == Issue.NodeGuid) + { + FKismetEditorUtilities::BringKismetToFocusAttentionOnObject(Node); + return; + } + } + } + + FKismetEditorUtilities::BringKismetToFocusAttentionOnObject(Blueprint); + } +} \ No newline at end of file diff --git a/Source/EasyLocalizationToolEditor/Private/ELTEditorAuditor.cpp b/Source/EasyLocalizationToolEditor/Private/ELTEditorAuditor.cpp new file mode 100644 index 0000000..c0fa93f --- /dev/null +++ b/Source/EasyLocalizationToolEditor/Private/ELTEditorAuditor.cpp @@ -0,0 +1,484 @@ +// Copyright (c) 2026 Crezetique. All rights reserved. + +#include "ELTEditorAuditor.h" +#include "ELTBlueprintLibrary.h" +#include "ELTEditorAuditWidget.h" + +#include "EditorUtilityLibrary.h" + +#include "AssetRegistry/AssetRegistryModule.h" +#include "Engine/AssetManager.h" + +#include "Internationalization/StringTableCore.h" +#include "Internationalization/StringTableRegistry.h" +#include "Internationalization/TextKey.h" +#include "Runtime/Launch/Resources/Version.h" + +#include "Engine/Blueprint.h" +#include "StructUtils/UserDefinedStruct.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "EdGraph/EdGraphPin.h" +#include "K2Node_FunctionEntry.h" +#include "WidgetBlueprint.h" +#include "Blueprint/WidgetTree.h" +#include "Components/Widget.h" + +DEFINE_LOG_CATEGORY_STATIC(ELTAuditLog, Log, All); + +namespace ELTAuditInternal +{ + static FString NodeDisplayName(const UEdGraphNode* Node) + { + return Node->GetNodeTitle(ENodeTitleType::ListView).ToString(); + } +} + +/*static*/ void UELTEditorAuditor::AuditText( + const FText& InText, + EELTAuditParameterType ParameterType, + const FString& VariableNodeName, + const FGuid& NodeGuid, + const FString& GraphName, + FELTAssetAuditResult& OutResult) +{ + // Skip if localized is set to false + if (InText.IsCultureInvariant()) + { + return; + } + + FString Package, Namespace, Key, Source; + UELTBlueprintLibrary::GetTextData(InText, Package, Namespace, Key, Source); + + auto AddIssue = [&](EELTAuditIssueType IssueType, FString LocalizedString = TEXT(""), bool bIsUsingStringTable = false) + { + FELTAuditIssue& Row = OutResult.Issues.Emplace_GetRef(); + Row.AssetName = OutResult.AssetData.AssetName.ToString(); + Row.Type = ParameterType; + Row.VariableNodeName = VariableNodeName; + Row.Issue = IssueType; + Row.Value = Source; + Row.LocalizedString = MoveTemp(LocalizedString); + Row.Key = Key; + Row.Namespace = Namespace; + Row.bIsUsingStringTable = bIsUsingStringTable; + Row.NodeGuid = NodeGuid; + Row.GraphName = GraphName; + }; + + if (InText.IsEmpty()) + { + AddIssue(EELTAuditIssueType::EmptyValue); + return; + } + + // Check for a broken String Table reference before valid localization check + FName TableId; + FString TableKey; + if (FTextInspector::GetTableIdAndKey(InText, TableId, TableKey)) + { + Key = TableKey; + Namespace = TableId.ToString(); + + FStringTableConstPtr Table = FStringTableRegistry::Get().FindStringTable(TableId); + bool bKeyValid = Table.IsValid() && Table->FindEntry(TableKey).IsValid(); + if (bKeyValid == false) + { + AddIssue(EELTAuditIssueType::StringTableMissingKey, TEXT(""), true); + } + else + { + AddIssue(EELTAuditIssueType::None, InText.ToString(), true); + } + return; + } + + // We assume key missing or key-value mismatched means the text was never set up to fetch localization strings via ELT + if (Key.IsEmpty() || Key.Equals(Source, ESearchCase::CaseSensitive) == false) + { + AddIssue(EELTAuditIssueType::NotYetLocalized); + return; + } + + FText FoundText; + bool bFoundInTable; + +#if ((ENGINE_MAJOR_VERSION == 5) && (ENGINE_MINOR_VERSION >= 5)) + bFoundInTable = FText::FindTextInLiveTable_Advanced(*Namespace, *Key, FoundText, &Source); +#else + bFoundInTable = FText::FindText(*Namespace, *Key, FoundText, &Source); +#endif + + if (bFoundInTable) + { + AddIssue(EELTAuditIssueType::None, FoundText.ToString()); + } + else + { + AddIssue(EELTAuditIssueType::InvalidLocalization); + } +} + +/*static*/ void UELTEditorAuditor::AuditClassProperties( + void* ContainerPtr, + UStruct* Struct, + const FString& PathPrefix, + FELTAssetAuditResult& OutResult) +{ + for (TFieldIterator It(Struct, EFieldIteratorFlags::IncludeSuper); It; ++It) + { + FProperty* Prop = *It; + + const FString PropName = Cast(Struct) + ? Prop->GetAuthoredName() + : Prop->GetName(); + + const FString CurrentPath = PathPrefix.IsEmpty() ? PropName : PathPrefix + TEXT(".") + PropName; + + if (const FTextProperty* TextProp = CastField(Prop)) + { + AuditText(TextProp->GetPropertyValue_InContainer(ContainerPtr), + EELTAuditParameterType::ClassVariable, CurrentPath, FGuid(), TEXT(""), OutResult); + } + else if (const FStructProperty* StructProp = CastField(Prop)) + { + AuditClassProperties(StructProp->ContainerPtrToValuePtr(ContainerPtr), StructProp->Struct, CurrentPath, OutResult); + } + else if (const FArrayProperty* ArrayProp = CastField(Prop)) + { + FScriptArrayHelper ArrayHelper(ArrayProp, ArrayProp->ContainerPtrToValuePtr(ContainerPtr)); + for (int32 i = 0; i < ArrayHelper.Num(); i++) + { + const FString ElemPath = FString::Printf(TEXT("%s[%d]"), *CurrentPath, i); + void* ElemPtr = ArrayHelper.GetRawPtr(i); + + if (const FTextProperty* InnerText = CastField(ArrayProp->Inner)) + { + AuditText(InnerText->GetPropertyValue(ElemPtr), + EELTAuditParameterType::ClassVariable, ElemPath, FGuid(), TEXT(""), OutResult); + } + else if (const FStructProperty* InnerStruct = CastField(ArrayProp->Inner)) + { + AuditClassProperties(ElemPtr, InnerStruct->Struct, ElemPath, OutResult); + } + } + } + else if (const FMapProperty* MapProp = CastField(Prop)) + { + FScriptMapHelper MapHelper(MapProp, MapProp->ContainerPtrToValuePtr(ContainerPtr)); + for (int32 i = 0; i < MapHelper.GetMaxIndex(); i++) + { + if (MapHelper.IsValidIndex(i) == false) { continue; } + + const FString ElemPath = FString::Printf(TEXT("%s[%d].Value"), *CurrentPath, i); + void* ValPtr = MapHelper.GetValuePtr(i); + + if (const FTextProperty* ValText = CastField(MapProp->ValueProp)) + { + AuditText(ValText->GetPropertyValue(ValPtr), + EELTAuditParameterType::ClassVariable, ElemPath, FGuid(), TEXT(""), OutResult); + } + else if (const FStructProperty* ValStruct = CastField(MapProp->ValueProp)) + { + AuditClassProperties(ValPtr, ValStruct->Struct, ElemPath, OutResult); + } + } + } + } +} + +/*static*/ void UELTEditorAuditor::AuditBlueprintTextSources( + UBlueprint* Blueprint, + FELTAssetAuditResult& OutResult) +{ + if (Blueprint == nullptr) + { + return; + } + + TArray AllGraphs; + Blueprint->GetAllGraphs(AllGraphs); + + for (UEdGraph* Graph : AllGraphs) + { + if (Graph == nullptr) { continue; } + + const FString GraphName = Graph->GetName(); + + for (UEdGraphNode* Node : Graph->Nodes) + { + if (Node == nullptr) { continue; } + + if (UK2Node_FunctionEntry* EntryNode = Cast(Node)) + { + // Local variables are stored on the entry node + for (const FBPVariableDescription& LocalVar : EntryNode->LocalVariables) + { + if (LocalVar.VarType.PinCategory != UEdGraphSchema_K2::PC_Text) { continue; } + + FText DefaultText; + if (FTextStringHelper::ReadFromBuffer(*LocalVar.DefaultValue, DefaultText) && DefaultText.IsEmpty() == false) + { + AuditText(DefaultText, EELTAuditParameterType::FunctionVariable, + FString::Printf(TEXT("%s::%s"), *GraphName, *LocalVar.VarName.ToString()), + EntryNode->NodeGuid, GraphName, OutResult); + } + } + + // Parameter pins default values + for (UEdGraphPin* Pin : EntryNode->Pins) + { + if (Pin == nullptr || Pin->PinType.PinCategory != UEdGraphSchema_K2::PC_Text) { continue; } + if (Pin->DefaultTextValue.IsEmpty()) { continue; } + + AuditText(Pin->DefaultTextValue, EELTAuditParameterType::FunctionVariable, + FString::Printf(TEXT("%s::%s"), *GraphName, *Pin->PinName.ToString()), + EntryNode->NodeGuid, GraphName, OutResult); + } + + continue; + } + + // All other nodes: unconnected literal FText input pins. + for (UEdGraphPin* Pin : Node->Pins) + { + if (Pin == nullptr) { continue; } + if (Pin->Direction != EGPD_Input) { continue; } + if (Pin->PinType.PinCategory != UEdGraphSchema_K2::PC_Text) { continue; } + if (Pin->LinkedTo.Num() > 0) { continue; } + if (Pin->DefaultTextValue.IsEmpty()) { continue; } + + AuditText(Pin->DefaultTextValue, EELTAuditParameterType::NodeParameter, + ELTAuditInternal::NodeDisplayName(Node), Node->NodeGuid, GraphName, OutResult); + } + } + } +} + +/*static*/ void UELTEditorAuditor::AuditSingleAsset( + UObject* Asset, + const FAssetData& AssetData, + FELTAssetAuditResult& OutResult) +{ + OutResult.AssetData = AssetData; + + if (UBlueprint* Blueprint = Cast(Asset)) + { + // Covers function locals, parameter pins, and nodes. + AuditBlueprintTextSources(Blueprint, OutResult); + + // Covers UMG components. + if (UWidgetBlueprint* WidgetBlueprint = Cast(Blueprint)) + { + AuditWidgetComponents(WidgetBlueprint, OutResult); + } + + // Covers CDO + // (Note: Class Properties will not suffice for Blueprint Assets) + UObject* CDO = Blueprint->GeneratedClass ? Blueprint->GeneratedClass->GetDefaultObject(false) : nullptr; + if (CDO != nullptr) + { + for (const FBPVariableDescription& Var : Blueprint->NewVariables) + { + if (Var.VarType.PinCategory != UEdGraphSchema_K2::PC_Text) { continue; } + + FProperty* Prop = Blueprint->GeneratedClass->FindPropertyByName(Var.VarName); + if (const FTextProperty* TextProp = CastField(Prop)) + { + const FText& Val = TextProp->GetPropertyValue_InContainer(CDO); + AuditText(Val, EELTAuditParameterType::ClassVariable, + Var.VarName.ToString(), FGuid(), TEXT(""), OutResult); + } + } + } + } + else if (UUserDefinedStruct* UserStruct = Cast(Asset)) + { + if (const uint8* DefaultInstance = UserStruct->GetDefaultInstance()) + { + AuditClassProperties( + const_cast(DefaultInstance), + UserStruct, + TEXT(""), + OutResult); + } + } + else + { + // Non-Blueprint Asset, Audit Class Defaults + AuditClassProperties(Asset, Asset->GetClass(), TEXT(""), OutResult); + } +} + +/*static*/ void UELTEditorAuditor::AuditWidgetComponents( + UWidgetBlueprint* WidgetBlueprint, + FELTAssetAuditResult& OutResult) +{ + if (WidgetBlueprint == nullptr || WidgetBlueprint->WidgetTree == nullptr) + { + return; + } + + WidgetBlueprint->WidgetTree->ForEachWidget([&](UWidget* Widget) + { + if (Widget == nullptr) { return; } + + const FString ComponentName = Widget->GetName(); + + for (TFieldIterator PropIt(Widget->GetClass(), EFieldIteratorFlags::IncludeSuper); PropIt; ++PropIt) + { + const FTextProperty* TextProp = *PropIt; + const FName PropName = TextProp->GetFName(); + + // ToolTipText is intentionally empty on most widgets, skip if unset. + if (PropName == FName("ToolTipText")) + { + const FText& Val = TextProp->GetPropertyValue_InContainer(Widget); + if (Val.IsEmpty() == false) + { + AuditText(Val, EELTAuditParameterType::WidgetComponent, + FString::Printf(TEXT("%s.ToolTipText"), *ComponentName), FGuid(), TEXT(""), OutResult); + } + continue; + } + + // Accessible text fields are only relevant if set to override them with custom accessibility. + if (PropName == FName("AccessibleText") || PropName == FName("AccessibleSummaryText")) + { +#if (ENGINE_MAJOR_VERSION >= 5) + if (Widget->bOverrideAccessibleDefaults == false) { continue; } + + const bool bIsAccessible = (PropName == FName("AccessibleText")) && (Widget->AccessibleBehavior == ESlateAccessibleBehavior::Custom); + const bool bIsSummaryAccessible = (PropName == FName("AccessibleSummaryText")) && (Widget->AccessibleSummaryBehavior == ESlateAccessibleBehavior::Custom); + + if (bIsAccessible == false && bIsSummaryAccessible == false) { continue; } +#else + // Accessible override fields not supported in UE4 — skip entirely. + continue; +#endif + } + + // CommonUI internal classification hidden field — not a localizable text value. + if (PropName == FName("PaletteCategory")) { continue; } + + const FText& Val = TextProp->GetPropertyValue_InContainer(Widget); + AuditText(Val, EELTAuditParameterType::WidgetComponent, + FString::Printf(TEXT("%s.%s"), *ComponentName, *PropName.ToString()), FGuid(), TEXT(""), OutResult); + } + }); +} + +TSharedPtr UELTEditorAuditor::PendingLoadHandle; + +/*static*/ TArray UELTEditorAuditor::RunAudit(const TArray& Assets) +{ + TArray Results; + + for (const FAssetData& AssetData : Assets) + { + UObject* Asset = AssetData.GetAsset(); + if (Asset == nullptr) { continue; } + + FELTAssetAuditResult& Result = Results.Emplace_GetRef(); + AuditSingleAsset(Asset, AssetData, Result); + } + + UE_LOG(ELTAuditLog, Log, TEXT("Localization audit complete. %d asset(s) scanned."), Assets.Num()); + return Results; +} + +static void RunAuditAndShow(const TArray& Assets) +{ + const TArray Results = UELTEditorAuditor::RunAudit(Assets); + UELTEditorAuditWidget::ShowResults(Results); +} + +static void RequestLoadThenAudit(const TArray& Assets) +{ + // Cancel any in-progress load — the new request supersedes it. + if (UELTEditorAuditor::PendingLoadHandle.IsValid()) + { + UELTEditorAuditor::PendingLoadHandle->CancelHandle(); + UELTEditorAuditor::PendingLoadHandle.Reset(); + } + + TArray Unloaded; + for (const FAssetData& AssetData : Assets) + { + if (AssetData.IsAssetLoaded() == false) + { + Unloaded.Add(AssetData); + } + } + + if (Unloaded.IsEmpty()) + { + RunAuditAndShow(Assets); + return; + } + + // Build soft object paths for all unloaded assets and request an async load. + // PendingLoadHandle keeps the request alive until the callback fires. + TArray PathsToLoad; + for (const FAssetData& AssetData : Unloaded) + { + PathsToLoad.Add(AssetData.ToSoftObjectPath()); + } + + UE_LOG(ELTAuditLog, Log, TEXT("Waiting for %d asset(s) to load before auditing..."), Unloaded.Num()); + + UELTEditorAuditor::PendingLoadHandle = UAssetManager::GetStreamableManager().RequestAsyncLoad( + PathsToLoad, + FStreamableDelegate::CreateLambda([Assets]() + { + UELTEditorAuditor::PendingLoadHandle.Reset(); + RunAuditAndShow(Assets); + }) + ); +} + +void UELTEditorAuditor::AuditSelectedAssets() +{ + TArray Assets = UEditorUtilityLibrary::GetSelectedAssetData(); + +#if (ENGINE_MAJOR_VERSION >= 5) + const TArray SelectedPaths = UEditorUtilityLibrary::GetSelectedFolderPaths(); + if (SelectedPaths.Num() > 0) + { + FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked(TEXT("AssetRegistry")); + IAssetRegistry& AssetRegistry = AssetRegistryModule.Get(); + + FARFilter Filter; + Filter.bRecursivePaths = true; + for (const FString& Path : SelectedPaths) + { + FString PackagePath = Path; + const FString AllPrefix = TEXT("/All"); + if (PackagePath.StartsWith(AllPrefix)) + { + PackagePath = PackagePath.RightChop(AllPrefix.Len()); + } + Filter.PackagePaths.Add(FName(*PackagePath)); + } + + TArray FolderAssets; + AssetRegistry.GetAssets(Filter, FolderAssets); + + for (const FAssetData& FolderAsset : FolderAssets) + { + Assets.AddUnique(FolderAsset); + } + } +#endif + + // Nothing selected — do nothing rather than opening an empty audit. + if (Assets.IsEmpty()) { return; } + + RequestLoadThenAudit(Assets); +} + +void UELTEditorAuditor::AuditAssets(const TArray& Assets) +{ + RequestLoadThenAudit(Assets); +} \ No newline at end of file diff --git a/Source/EasyLocalizationToolEditor/Private/ELTEditorCommands.cpp b/Source/EasyLocalizationToolEditor/Private/ELTEditorCommands.cpp index 0d71b8c..8f026e2 100644 --- a/Source/EasyLocalizationToolEditor/Private/ELTEditorCommands.cpp +++ b/Source/EasyLocalizationToolEditor/Private/ELTEditorCommands.cpp @@ -22,6 +22,7 @@ void FELTEditorCommands::RegisterCommands() { #define LOCTEXT_NAMESPACE "ELTLoc" UI_COMMAND(OpenELTMenu, "Easy Localization Tool", "Opens Easy Localisation Tool Editor Window", EUserInterfaceActionType::Check, FInputChord(EModifierKey::Shift | EModifierKey::Alt, EKeys::L)); + UI_COMMAND(RunELTAudit, "Audit Localizations", "Audit selected assets if content browser is focused, otherwise audit focused editor window if it is an asset.", EUserInterfaceActionType::Check, FInputChord(EModifierKey::Control | EModifierKey::Alt, EKeys::L)); #undef LOCTEXT_NAMESPACE } diff --git a/Source/EasyLocalizationToolEditor/Private/ELTEditorSettings.cpp b/Source/EasyLocalizationToolEditor/Private/ELTEditorSettings.cpp index 6a74ded..c7b1704 100644 --- a/Source/EasyLocalizationToolEditor/Private/ELTEditorSettings.cpp +++ b/Source/EasyLocalizationToolEditor/Private/ELTEditorSettings.cpp @@ -72,6 +72,16 @@ void UELTEditorSettings::SetReimportAtEditorStartup(bool bNewReimportAtEditorSta ELTE_SET_SETTING(bReimportAtEditorStartup, bNewReimportAtEditorStartup); } +bool UELTEditorSettings::GetGenerateKeyReferenceStringTable() +{ + ELTE_GET_SETTING(bGenerateKeyReferenceStringTable); +} + +void UELTEditorSettings::SetGenerateKeyReferenceStringTable(bool bNewGenerateKeyReferenceStringTable) +{ + ELTE_SET_SETTING(bGenerateKeyReferenceStringTable, bNewGenerateKeyReferenceStringTable); +} + bool UELTEditorSettings::GetPreviewInUIEnabled() { ELTE_GET_SETTING(bPreviewInUI); diff --git a/Source/EasyLocalizationToolEditor/Private/ELTEditorWidget.cpp b/Source/EasyLocalizationToolEditor/Private/ELTEditorWidget.cpp index c3d0b07..af514c3 100644 --- a/Source/EasyLocalizationToolEditor/Private/ELTEditorWidget.cpp +++ b/Source/EasyLocalizationToolEditor/Private/ELTEditorWidget.cpp @@ -153,6 +153,14 @@ void UELTEditorWidget::CallSetLocalizationOnFirstRun(bool LocalizationOnFirstRun #endif } +void UELTEditorWidget::CallSetGenerateKeyReferenceStringTable(bool bGenerateKeyReferenceStringTable) +{ + if (MyWidget.IsValid()) + { + MyWidget->SetGenerateKeyReferenceStringTable(bGenerateKeyReferenceStringTable); + } +} + void UELTEditorWidget::CallSetLocalizationOnFirstRunLang(const FString& OnFirstRunLang) { #if ELTEDITOR_USE_SLATE_EDITOR_UI @@ -281,6 +289,11 @@ void UELTEditorWidget::OnLocalizationOnFirstRunLangChanged(const FString& OnFirs OnLocalizationOnFirstRunLangChangedDelegate.ExecuteIfBound(OnFirstRunLang); } +void UELTEditorWidget::OnGenerateKeyReferenceStringTableChanged(bool bGenerateKeyReferenceStringTable) +{ + OnGenerateKeyReferenceStringTableChangedDelegate.ExecuteIfBound(bGenerateKeyReferenceStringTable); +} + void UELTEditorWidget::OnGlobalNamespaceChanged(const FString& NewGlobalNamespace) { OnGlobalNamespaceChangedDelegate.ExecuteIfBound(NewGlobalNamespace); diff --git a/Source/EasyLocalizationToolEditor/Private/EasyLocalizationToolEditorModule.cpp b/Source/EasyLocalizationToolEditor/Private/EasyLocalizationToolEditorModule.cpp index 134a5ef..d7b147f 100644 --- a/Source/EasyLocalizationToolEditor/Private/EasyLocalizationToolEditorModule.cpp +++ b/Source/EasyLocalizationToolEditor/Private/EasyLocalizationToolEditorModule.cpp @@ -14,10 +14,19 @@ #include "PropertyEditorModule.h" #include "LevelEditor.h" -#include "BlueprintEditorModule.h" +#include "ContentBrowserModule.h" +#include "ContentBrowserDelegates.h" +#include "IContentBrowserSingleton.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "ELTEditorAuditor.h" +#include "Subsystems/AssetEditorSubsystem.h" +#include "Toolkits/AssetEditorToolkit.h" +#include "Editor.h" #if ((ENGINE_MAJOR_VERSION == 5) && (ENGINE_MINOR_VERSION >= 4)) #include "ToolMenu.h" +#include "Toolkits/AssetEditorToolkitMenuContext.h" +#include "Toolkits/AssetEditorToolkit.h" #endif IMPLEMENT_MODULE(FEasyLocalizationToolEditorModule, EasyLocalizationToolEditor) @@ -61,6 +70,105 @@ void FEasyLocalizationToolEditorModule::StartupModule() GraphPanelPinFactory = MakeShared(); FEdGraphUtilities::RegisterVisualPinFactory(GraphPanelPinFactory); #endif + + // Register the Localization Audit action in the Content Browser context menu. + FContentBrowserMenuExtender_SelectedAssets AuditExtender = FContentBrowserMenuExtender_SelectedAssets::CreateLambda( + [](const TArray& SelectedAssets) -> TSharedRef + { + TSharedRef Extender = MakeShared(); + Extender->AddMenuExtension( + "CommonAssetActions", + EExtensionHook::After, + nullptr, + FMenuExtensionDelegate::CreateLambda([](FMenuBuilder& MenuBuilder) + { + MenuBuilder.BeginSection("ELTLocalizationAudit", FText::FromString("Easy Localization Tool")); + MenuBuilder.AddMenuEntry( + FText::FromString("Localization Audit"), + FText::FromString("\ +Scans asset(s) for all FText properties marked to be localize, and returns them in a list.\n\n\ +They are tested for the following issues:\n\ +1. Empty Text Value\n\ +2. Invalid String Table Key Reference\n\ +3. Not Yet Localized (Value and Localization Key do not match)\n\ +4. Invalid Localization (Value and Localization Key matches, but does not return a valid localization string)"), + FSlateIcon(), + FUIAction(FExecuteAction::CreateLambda([]() + { + GetMutableDefault()->AuditSelectedAssets(); + })) + ); + MenuBuilder.EndSection(); + }) + ); + return Extender; + } + ); + ContentBrowserExtenderHandle = AuditExtender.GetHandle(); + FModuleManager::LoadModuleChecked(TEXT("ContentBrowser")) + .GetAllAssetViewContextMenuExtenders().Add(MoveTemp(AuditExtender)); + + FContentBrowserMenuExtender_SelectedPaths FolderAuditExtender = FContentBrowserMenuExtender_SelectedPaths::CreateLambda( + [](const TArray& SelectedPaths) -> TSharedRef + { + TSharedRef Extender = MakeShared(); + Extender->AddMenuExtension( + "PathViewFolderOptions", + EExtensionHook::After, + nullptr, + FMenuExtensionDelegate::CreateLambda([SelectedPaths](FMenuBuilder& MenuBuilder) + { + MenuBuilder.BeginSection("ELTLocalizationAudit", FText::FromString("Easy Localization Tool")); + MenuBuilder.AddMenuEntry( + FText::FromString("Localization Audit"), + FText::FromString("Scans all assets in the selected folder(s) for FText localization issues."), + FSlateIcon(), + FUIAction(FExecuteAction::CreateLambda([SelectedPaths]() + { + FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked(TEXT("AssetRegistry")); + IAssetRegistry& AssetRegistry = AssetRegistryModule.Get(); + + FARFilter Filter; + Filter.bRecursivePaths = true; + for (const FString& Path : SelectedPaths) + { + // GetAllPathViewContextMenuExtenders also returns /All-prefixed + // virtual paths — strip to get the real package path. + FString PackagePath = Path; + const FString AllPrefix = TEXT("/All"); + if (PackagePath.StartsWith(AllPrefix)) + { + PackagePath = PackagePath.RightChop(AllPrefix.Len()); + } + Filter.PackagePaths.Add(FName(*PackagePath)); + } + + TArray Assets; + AssetRegistry.GetAssets(Filter, Assets); + + // Also include any assets selected in the asset view — the path view + // extender fires when right-clicking a folder regardless of asset selection, + // so we need to merge both selections manually. + TArray SelectedAssets; + FModuleManager::LoadModuleChecked(TEXT("ContentBrowser")) + .Get().GetSelectedAssets(SelectedAssets); + for (const FAssetData& SelectedAsset : SelectedAssets) + { + Assets.AddUnique(SelectedAsset); + } + + GetMutableDefault()->AuditAssets(Assets); + })) + ); + MenuBuilder.EndSection(); + }) + ); + return Extender; + } + ); + PathViewExtenderHandle = FolderAuditExtender.GetHandle(); + FModuleManager::LoadModuleChecked(TEXT("ContentBrowser")) + .GetAllPathViewContextMenuExtenders().Add(MoveTemp(FolderAuditExtender)); } void FEasyLocalizationToolEditorModule::ShutdownModule() @@ -80,6 +188,23 @@ void FEasyLocalizationToolEditorModule::ShutdownModule() PropertyModule.UnregisterCustomClassLayout(UObject::StaticClass()->GetFName()); #endif + // Unregister the Localization Audit content browser extender. + if (FContentBrowserModule* CBModule = FModuleManager::GetModulePtr(TEXT("ContentBrowser"))) + { + CBModule->GetAllAssetViewContextMenuExtenders().RemoveAll( + [this](const FContentBrowserMenuExtender_SelectedAssets& Delegate) + { + return Delegate.GetHandle() == ContentBrowserExtenderHandle; + } + ); + CBModule->GetAllPathViewContextMenuExtenders().RemoveAll( + [this](const FContentBrowserMenuExtender_SelectedPaths& Delegate) + { + return Delegate.GetHandle() == PathViewExtenderHandle; + } + ); + } + // Unregister Tab Spawner FGlobalTabmanager::Get()->UnregisterNomadTabSpawner(ELTTabId); @@ -100,6 +225,82 @@ void FEasyLocalizationToolEditorModule::ShutdownModule() FELTEditorStyle::Shutdown(); } +void FEasyLocalizationToolEditorModule::RunAuditCommand() +{ + if (!FSlateApplication::IsInitialized() || !GEditor) + { + return; + } + + TArray AssetsToAudit; + bool bContentBrowserHasFocus = false; + + // Check if the active window/widget is a Content Browser or Content Drawer + TSharedPtr FocusedWidget = FSlateApplication::Get().GetUserFocusedWidget(0); + if (FocusedWidget.IsValid()) + { + FWidgetPath FocusedWidgetPath; + if (FSlateApplication::Get().GeneratePathToWidgetUnchecked(FocusedWidget.ToSharedRef(), FocusedWidgetPath)) + { + for (int32 i = FocusedWidgetPath.Widgets.Num() - 1; i >= 0; --i) + { + FString WidgetType = FocusedWidgetPath.Widgets[i].Widget->GetTypeAsString(); + + // This catches active standalone content browsers and the pop-up content drawer + if (WidgetType.Contains("SContentBrowser") || WidgetType.Contains("SContentDrawer")) + { + bContentBrowserHasFocus = true; + break; + } + } + } + } + + // If bContentBrowserHasFocus is true, audit selected assets and folders. Does nothing if nothing is selected. + if (bContentBrowserHasFocus) + { + GetMutableDefault()->AuditSelectedAssets(); + return; + } + + // Check if the active window is an asset, audit it if true. + UAssetEditorSubsystem* AssetEditorSubsystem = GEditor->GetEditorSubsystem(); + if (AssetEditorSubsystem == nullptr) + { + return; + } + + // Loop through all opened assets and check if the asset window is current tab. + for (UObject* Asset : AssetEditorSubsystem->GetAllEditedAssets()) + { + IAssetEditorInstance* EditorInstance = AssetEditorSubsystem->FindEditorForAsset(Asset, false); + if (!EditorInstance) + { + continue; + } + + FAssetEditorToolkit* EditorToolkit = static_cast(EditorInstance); + if (!EditorToolkit) + { + continue; + } + + TSharedPtr TabManager = EditorToolkit->GetTabManager(); + if (!TabManager) + { + continue; + } + + TSharedPtr OwnerTab = TabManager->GetOwnerTab(); + if (OwnerTab.IsValid() && OwnerTab->IsForeground()) + { + AssetsToAudit.Add(FAssetData(Asset)); + GetMutableDefault()->AuditAssets(AssetsToAudit); + return; + } + } +} + void FEasyLocalizationToolEditorModule::OnPostEngineInit() { // This function is for registering UICommand to the engine, so it can be executed via keyboard shortcut. @@ -119,6 +320,12 @@ void FEasyLocalizationToolEditorModule::OnPostEngineInit() FIsActionChecked::CreateRaw(this, &FEasyLocalizationToolEditorModule::IsEditorSpawned) ); + // Map the audit shortcut to RunAuditCommand. + Commands->MapAction( + FELTEditorCommands::Get().RunELTAudit, + FExecuteAction::CreateRaw(this, &FEasyLocalizationToolEditorModule::RunAuditCommand) + ); + // Register this UICommandList to the MainFrame. // Otherwise nothing will handle the input to trigger this command. IMainFrameModule& MainFrame = FModuleManager::Get().LoadModuleChecked("MainFrame"); @@ -126,17 +333,35 @@ void FEasyLocalizationToolEditorModule::OnPostEngineInit() #if ((ENGINE_MAJOR_VERSION == 5) && (ENGINE_MINOR_VERSION >= 4)) - // The Menu Extender doesn't work correctly for new menus in UE5.4 as they don't have proper Hook names as they should... - // Attempt to add menu entry with UICommandList of opening ELT Window to the Tools menu. - UToolMenu* Menu = UToolMenus::Get()->FindMenu("LevelEditor.MainMenu.Tools"); - if (Menu) + // Add both the ELT window opener and Localization Audit to the same named section + // under the Tools menu, so they appear grouped as "Easy Localization Tool" entries. + UToolMenu* ToolsMenu = UToolMenus::Get()->ExtendMenu(TEXT("MainFrame.MainMenu.Tools")); + if (ToolsMenu) { - Menu->AddMenuEntry(NAME_None, FToolMenuEntry::InitMenuEntryWithCommandList( - FELTEditorCommands::Get().OpenELTMenu, + FToolMenuSection& Section = ToolsMenu->FindOrAddSection( + "ELTLocalizationAudit", + FText::FromString(TEXT("Easy Localization Tool")) + ); + + Section.AddEntry(FToolMenuEntry::InitMenuEntryWithCommandList( + FELTEditorCommands::Get().OpenELTMenu, Commands, FText::FromString(TEXT("Easy Localization Tool")), FText::FromString(TEXT("Opens Easy Localization Tool Window")), FSlateIcon(FELTEditorStyle::GetStyleSetName(), "ELTEditorStyle.MenuIcon"))); + + Section.AddEntry(FToolMenuEntry::InitMenuEntryWithCommandList( + FELTEditorCommands::Get().RunELTAudit, + Commands, + FText::FromString(TEXT("Localization Audit")), + FText::FromString(TEXT("\ +Scans this asset for all FText properties marked to be localize, and returns them in a list.\n\n\ +They are tested for the following issues:\n\ +1. Empty Text Value\n\ +2. Invalid String Table Key Reference\n\ +3. Not Yet Localized (Value and Localization Key do not match)\n\ +4. Invalid Localization (Value and Localization Key matches, but does not return a valid localization string)")), + FSlateIcon(FELTEditorStyle::GetStyleSetName(), "ELTEditorStyle.MenuIcon"))); } #else // Create a Menu Extender, which adds a button that executes the UICommandList of opening ELT Window. diff --git a/Source/EasyLocalizationToolEditor/Private/SELTEditorWidget.cpp b/Source/EasyLocalizationToolEditor/Private/SELTEditorWidget.cpp index 5f59a13..d443a69 100644 --- a/Source/EasyLocalizationToolEditor/Private/SELTEditorWidget.cpp +++ b/Source/EasyLocalizationToolEditor/Private/SELTEditorWidget.cpp @@ -16,7 +16,7 @@ void SELTEditorWidget::Construct(const FArguments& InArgs) FallbackWhenEmptyAvailable.Add(MakeShareable(new FString(TEXT("KEY")))); SelectedFallbackWhenEmpty = FallbackWhenEmptyAvailable[0]; - SpacerBrush.SetImageSize(FVector2D(350.f, 1.f)); + SpacerBrush.SetImageSize(FVector2D(600.f, 1.f)); SpacerBrush.TintColor = FSlateColor(FLinearColor(.62f,.62f,.62f,1.f)); SUserWidget::Construct(SUserWidget::FArguments() @@ -53,7 +53,7 @@ void SELTEditorWidget::Construct(const FArguments& InArgs) // > Spacer ================ +SVerticalBox::Slot() .AutoHeight() - .Padding(FMargin(0.f, 4.f, 0.f, 4.f)) + .Padding(FMargin(0.f, 15.f, 0.f, 15.f)) .HAlign(EHorizontalAlignment::HAlign_Left) .VAlign(EVerticalAlignment::VAlign_Center) [ @@ -65,8 +65,36 @@ void SELTEditorWidget::Construct(const FArguments& InArgs) [ SNew(SVerticalBox) .ToolTipText(INVTEXT("Name of currently selected Localization. The game can have multiple localization directories.")) + // >>>> Localization Name box + +SVerticalBox::Slot() + .AutoHeight() + [ + SNew(SHorizontalBox) + // >>>>>>>> Localization Name label + +SHorizontalBox::Slot() + .AutoWidth() + .Padding(FMargin(0.f, 5.f, 20.f, 0.f)) + [ + SNew(STextBlock) + .Font(FCoreStyle::GetDefaultFontStyle("Light", 11)) + .Text(INVTEXT("Localization name:")) + ] + // >>>>>>>> Localization Name value + +SHorizontalBox::Slot() + .AutoWidth() + .Padding(FMargin(0.f, 5.f, 20.f, 0.f)) + [ + SNew(STextBlock) + .Font(FCoreStyle::GetDefaultFontStyle("Bold", 11)) + .Text_Lambda([this]() -> FText + { + return FText::FromString(CurrentLocName); + }) + ] + ] // >>>> Localization Path selection list +SVerticalBox::Slot() + .Padding(FMargin(0.f, 3.f, 0.f, 0.f)) .AutoHeight() .HAlign(EHorizontalAlignment::HAlign_Left) [ @@ -97,38 +125,11 @@ void SELTEditorWidget::Construct(const FArguments& InArgs) }) ] ] - // >>>> Localization Name box - +SVerticalBox::Slot() - .AutoHeight() - [ - SNew(SHorizontalBox) - // >>>>>>>> Localization Name label - +SHorizontalBox::Slot() - .AutoWidth() - .Padding(FMargin(0.f, 0.f, 20.f, 0.f)) - [ - SNew(STextBlock) - .Font(FCoreStyle::GetDefaultFontStyle("Light", 12)) - .Text(INVTEXT("Localization name:")) - ] - // >>>>>>>> Localization Name value - +SHorizontalBox::Slot() - .AutoWidth() - .Padding(FMargin(0.f, 0.f, 20.f, 0.f)) - [ - SNew(STextBlock) - .Font(FCoreStyle::GetDefaultFontStyle("Bold", 12)) - .Text_Lambda([this]() -> FText - { - return FText::FromString(CurrentLocName); - }) - ] - ] ] // > Spacer ================ +SVerticalBox::Slot() .AutoHeight() - .Padding(FMargin(0.f, 4.f, 0.f, 4.f)) + .Padding(FMargin(0.f, 15.f, 0.f, 15.f)) .HAlign(EHorizontalAlignment::HAlign_Left) .VAlign(EVerticalAlignment::VAlign_Center) [ @@ -145,7 +146,7 @@ void SELTEditorWidget::Construct(const FArguments& InArgs) .AutoHeight() [ SNew(STextBlock) - .Font(FCoreStyle::GetDefaultFontStyle("Light", 12)) + .Font(FCoreStyle::GetDefaultFontStyle("Light", 11)) .Text(INVTEXT("Available Languages In Selected Localization:")) ] // >>>> Available Langs In Selected Localization Value @@ -153,7 +154,7 @@ void SELTEditorWidget::Construct(const FArguments& InArgs) .AutoHeight() [ SNew(STextBlock) - .Font(FCoreStyle::GetDefaultFontStyle("Bold", 12)) + .Font(FCoreStyle::GetDefaultFontStyle("Bold", 11)) .Text_Lambda([this]() -> FText { return FText::FromString(AvailableLangsInLocFile); @@ -168,10 +169,11 @@ void SELTEditorWidget::Construct(const FArguments& InArgs) .ToolTipText(INVTEXT("List of language codes that are implemented by every localization directory.")) // >>>> Available Langs Label +SVerticalBox::Slot() + .Padding(FMargin(0.f, 10.f, 0.f, 0)) .AutoHeight() [ SNew(STextBlock) - .Font(FCoreStyle::GetDefaultFontStyle("Light", 12)) + .Font(FCoreStyle::GetDefaultFontStyle("Light", 11)) .Text(INVTEXT("Available Languages:")) ] // >>>> Localization Names value @@ -179,7 +181,7 @@ void SELTEditorWidget::Construct(const FArguments& InArgs) .AutoHeight() [ SNew(STextBlock) - .Font(FCoreStyle::GetDefaultFontStyle("Bold", 12)) + .Font(FCoreStyle::GetDefaultFontStyle("Bold", 11)) .Text_Lambda([this]() -> FText { return FText::FromString(AvailableLangs); @@ -189,7 +191,7 @@ void SELTEditorWidget::Construct(const FArguments& InArgs) // > Spacer ================ +SVerticalBox::Slot() .AutoHeight() - .Padding(FMargin(0.f, 4.f, 0.f, 4.f)) + .Padding(FMargin(0.f, 15.f, 0.f, 15.f)) .HAlign(EHorizontalAlignment::HAlign_Left) .VAlign(EVerticalAlignment::VAlign_Center) [ @@ -199,96 +201,111 @@ void SELTEditorWidget::Construct(const FArguments& InArgs) +SVerticalBox::Slot() .AutoHeight() [ - SNew(SHorizontalBox) - .ToolTipText(INVTEXT("Reimports the lastly selected localization with the last used CSV file when editor starts.")) - // >>>> Reimport on editor startup Label - +SHorizontalBox::Slot() - .AutoWidth() - [ - SNew(STextBlock) - .Font(FCoreStyle::GetDefaultFontStyle("Light", 12)) - .Text(INVTEXT("Reimport on editor startup:")) - ] - // >>>> Reimport on editor startup checkbox - +SHorizontalBox::Slot() - .AutoWidth() + SNew(SBox) + .MinDesiredHeight(24.f) [ - SNew(SCheckBox) - .IsChecked_Lambda([this]() -> ECheckBoxState - { - return bReimportAtEditorStartup_Chkbox ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; - }) - .OnCheckStateChanged_Lambda([this](ECheckBoxState InCheckBoxState) -> void - { - bReimportAtEditorStartup_Chkbox = (InCheckBoxState == ECheckBoxState::Checked); - if (WidgetController.IsValid()) + SNew(SHorizontalBox) + .ToolTipText(INVTEXT("Reimports the lastly selected localization with the last used CSV file when editor starts.")) + // >>>> Reimport on editor startup Label + +SHorizontalBox::Slot() + .FillWidth(1.0f) + .MaxWidth(400.0f) + .VAlign(EVerticalAlignment::VAlign_Center) + [ + SNew(STextBlock) + .Font(FCoreStyle::GetDefaultFontStyle("Light", 11)) + .Text(INVTEXT("Reimport on editor startup:")) + ] + // >>>> Reimport on editor startup checkbox + +SHorizontalBox::Slot() + .AutoWidth() + .VAlign(EVerticalAlignment::VAlign_Center) + [ + SNew(SCheckBox) + .IsChecked_Lambda([this]() -> ECheckBoxState { - WidgetController->OnReimportAtEditorStartupChanged(bReimportAtEditorStartup_Chkbox); - } - }) + return bReimportAtEditorStartup_Chkbox ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }) + .OnCheckStateChanged_Lambda([this](ECheckBoxState InCheckBoxState) -> void + { + bReimportAtEditorStartup_Chkbox = (InCheckBoxState == ECheckBoxState::Checked); + if (WidgetController.IsValid()) + { + WidgetController->OnReimportAtEditorStartupChanged(bReimportAtEditorStartup_Chkbox); + } + }) + ] ] ] // > Localization Preview Box ================ +SVerticalBox::Slot() .AutoHeight() [ - SNew(SHorizontalBox) - .ToolTipText(INVTEXT("Enabled the preview of the localization in the editor.")) - // >>>> Localization Preview Label - +SHorizontalBox::Slot() - .AutoWidth() - [ - SNew(STextBlock) - .Font(FCoreStyle::GetDefaultFontStyle("Light", 12)) - .Text(INVTEXT("Localization Preview:")) - ] - // >>>> Localization Preview Checkbox - +SHorizontalBox::Slot() - .AutoWidth() + SNew(SBox) + .MinDesiredHeight(24.f) [ - SNew(SCheckBox) - .IsChecked_Lambda([this]() -> ECheckBoxState - { - return bIsLocalisationPreviewEnabled_Chkbox ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; - }) - .OnCheckStateChanged_Lambda([this](ECheckBoxState InCheckBoxState) -> void - { - bIsLocalisationPreviewEnabled_Chkbox = (InCheckBoxState == ECheckBoxState::Checked); - if (WidgetController.IsValid()) + SNew(SHorizontalBox) + .ToolTipText(INVTEXT("Enabled the preview of the localization in the editor.")) + // >>>> Localization Preview Label + +SHorizontalBox::Slot() + .FillWidth(1.0f) + .MaxWidth(400.0f) + .VAlign(EVerticalAlignment::VAlign_Center) + [ + SNew(STextBlock) + .Font(FCoreStyle::GetDefaultFontStyle("Light", 11)) + .Text(INVTEXT("Localization Preview:")) + ] + // >>>> Localization Preview Checkbox + +SHorizontalBox::Slot() + .AutoWidth() + .VAlign(EVerticalAlignment::VAlign_Center) + [ + SNew(SCheckBox) + .IsChecked_Lambda([this]() -> ECheckBoxState { - WidgetController->OnLocalizationPreviewChanged(bIsLocalisationPreviewEnabled_Chkbox); - } - }) - ] - // >>>> Localization Preview List - +SHorizontalBox::Slot() - .AutoWidth() - [ - SNew(SComboBox>) - .OptionsSource(&PreviewsAvailables) - .OnGenerateWidget_Lambda([this](TSharedPtr InItem) -> TSharedRef - { - return SNew(STextBlock).Text(FText::FromString(*InItem)); - }) - .OnSelectionChanged_Lambda([this](TSharedPtr Item, ESelectInfo::Type SelectInfo) -> void - { - SelectedPreviewLang = Item; - if (WidgetController.IsValid()) + return bIsLocalisationPreviewEnabled_Chkbox ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }) + .OnCheckStateChanged_Lambda([this](ECheckBoxState InCheckBoxState) -> void { - WidgetController->OnLocalizationPreviewLangChanged(*Item); - } - }) + bIsLocalisationPreviewEnabled_Chkbox = (InCheckBoxState == ECheckBoxState::Checked); + if (WidgetController.IsValid()) + { + WidgetController->OnLocalizationPreviewChanged(bIsLocalisationPreviewEnabled_Chkbox); + } + }) + ] + // >>>> Localization Preview List + +SHorizontalBox::Slot() + .AutoWidth() + .VAlign(EVerticalAlignment::VAlign_Center) [ - SNew(STextBlock) - .Font(FCoreStyle::GetDefaultFontStyle("Regular", 10)) - .Text_Lambda([this]() -> FText + SNew(SComboBox>) + .OptionsSource(&PreviewsAvailables) + .OnGenerateWidget_Lambda([this](TSharedPtr InItem) -> TSharedRef + { + return SNew(STextBlock).Text(FText::FromString(*InItem)); + }) + .OnSelectionChanged_Lambda([this](TSharedPtr Item, ESelectInfo::Type SelectInfo) -> void + { + SelectedPreviewLang = Item; + if (WidgetController.IsValid()) { - if (SelectedPreviewLang.IsValid()) + WidgetController->OnLocalizationPreviewLangChanged(*Item); + } + }) + [ + SNew(STextBlock) + .Font(FCoreStyle::GetDefaultFontStyle("Regular", 10)) + .Text_Lambda([this]() -> FText { - return FText::FromString(*SelectedPreviewLang); - } - return FText::GetEmpty(); - }) + if (SelectedPreviewLang.IsValid()) + { + return FText::FromString(*SelectedPreviewLang); + } + return FText::GetEmpty(); + }) + ] ] ] ] @@ -296,96 +313,111 @@ void SELTEditorWidget::Construct(const FArguments& InArgs) +SVerticalBox::Slot() .AutoHeight() [ - SNew(SHorizontalBox) - .ToolTipText(INVTEXT("If enabled it won't save and load lastly set language automatically.")) - // >>>> Manually Set Last Language Label - +SHorizontalBox::Slot() - .AutoWidth() + SNew(SBox) + .MinDesiredHeight(24.f) [ - SNew(STextBlock) - .Font(FCoreStyle::GetDefaultFontStyle("Light", 12)) - .Text(INVTEXT("Manually Set Last Language:")) - ] - // >>>> Manually Set Last Language checkbox - +SHorizontalBox::Slot() - .AutoWidth() - [ - SNew(SCheckBox) - .IsChecked_Lambda([this]() -> ECheckBoxState - { - return bManuallySetLastLanguage_Chkbox ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; - }) - .OnCheckStateChanged_Lambda([this](ECheckBoxState InCheckBoxState) -> void - { - bManuallySetLastLanguage_Chkbox = (InCheckBoxState == ECheckBoxState::Checked); - if (WidgetController.IsValid()) + SNew(SHorizontalBox) + .ToolTipText(INVTEXT("If enabled it won't save and load lastly set language automatically.")) + // >>>> Manually Set Last Language Label + +SHorizontalBox::Slot() + .FillWidth(1.0f) + .MaxWidth(400.0f) + .VAlign(EVerticalAlignment::VAlign_Center) + [ + SNew(STextBlock) + .Font(FCoreStyle::GetDefaultFontStyle("Light", 11)) + .Text(INVTEXT("Manually Set Last Language:")) + ] + // >>>> Manually Set Last Language checkbox + +SHorizontalBox::Slot() + .AutoWidth() + .VAlign(EVerticalAlignment::VAlign_Center) + [ + SNew(SCheckBox) + .IsChecked_Lambda([this]() -> ECheckBoxState { - WidgetController->OnManuallySetLastUsedLanguageChanged(bManuallySetLastLanguage_Chkbox); - } - }) + return bManuallySetLastLanguage_Chkbox ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }) + .OnCheckStateChanged_Lambda([this](ECheckBoxState InCheckBoxState) -> void + { + bManuallySetLastLanguage_Chkbox = (InCheckBoxState == ECheckBoxState::Checked); + if (WidgetController.IsValid()) + { + WidgetController->OnManuallySetLastUsedLanguageChanged(bManuallySetLastLanguage_Chkbox); + } + }) + ] ] ] // > Override Language on Startup Box ================ +SVerticalBox::Slot() .AutoHeight() [ - SNew(SHorizontalBox) - .ToolTipText(INVTEXT("If enabled, when the game starts for the very first time the selected language will be used.\nNormally, the system language will be used or it will fallback to \"en\".")) - // >>>> Override Language on Startup Label - +SHorizontalBox::Slot() - .AutoWidth() - [ - SNew(STextBlock) - .Font(FCoreStyle::GetDefaultFontStyle("Light", 12)) - .Text(INVTEXT("Override Language on Startup:")) - ] - // >>>> Override Language on Startup Checkbox - +SHorizontalBox::Slot() - .AutoWidth() + SNew(SBox) + .MinDesiredHeight(24.f) [ - SNew(SCheckBox) - .IsChecked_Lambda([this]() -> ECheckBoxState - { - return bOverrideLanguageOnStartup_Chkbox ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; - }) - .OnCheckStateChanged_Lambda([this](ECheckBoxState InCheckBoxState) -> void - { - bOverrideLanguageOnStartup_Chkbox = (InCheckBoxState == ECheckBoxState::Checked); - if (WidgetController.IsValid()) + SNew(SHorizontalBox) + .ToolTipText(INVTEXT("If enabled, when the game starts for the very first time the selected language will be used.\nNormally, the system language will be used or it will fallback to \"en\".")) + // >>>> Override Language on Startup Label + +SHorizontalBox::Slot() + .FillWidth(1.0f) + .MaxWidth(400.0f) + .VAlign(EVerticalAlignment::VAlign_Center) + [ + SNew(STextBlock) + .Font(FCoreStyle::GetDefaultFontStyle("Light", 11)) + .Text(INVTEXT("Override Language on Startup:")) + ] + // >>>> Override Language on Startup Checkbox + +SHorizontalBox::Slot() + .AutoWidth() + .VAlign(EVerticalAlignment::VAlign_Center) + [ + SNew(SCheckBox) + .IsChecked_Lambda([this]() -> ECheckBoxState { - WidgetController->OnLocalizationOnFirstRun(bOverrideLanguageOnStartup_Chkbox); - } - }) - ] - // >>>> Override Language on Startup List - +SHorizontalBox::Slot() - .AutoWidth() - [ - SNew(SComboBox>) - .OptionsSource(&LanguageOverridesAvailable) - .OnGenerateWidget_Lambda([this](TSharedPtr InItem) -> TSharedRef - { - return SNew(STextBlock).Text(FText::FromString(*InItem)); - }) - .OnSelectionChanged_Lambda([this](TSharedPtr Item, ESelectInfo::Type SelectInfo) -> void - { - SelectedLanguageOverride = Item; - if (WidgetController.IsValid()) + return bOverrideLanguageOnStartup_Chkbox ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }) + .OnCheckStateChanged_Lambda([this](ECheckBoxState InCheckBoxState) -> void { - WidgetController->OnLocalizationOnFirstRunLangChanged(*Item); - } - }) + bOverrideLanguageOnStartup_Chkbox = (InCheckBoxState == ECheckBoxState::Checked); + if (WidgetController.IsValid()) + { + WidgetController->OnLocalizationOnFirstRun(bOverrideLanguageOnStartup_Chkbox); + } + }) + ] + // >>>> Override Language on Startup List + +SHorizontalBox::Slot() + .AutoWidth() + .VAlign(EVerticalAlignment::VAlign_Center) [ - SNew(STextBlock) - .Font(FCoreStyle::GetDefaultFontStyle("Regular", 10)) - .Text_Lambda([this]() -> FText + SNew(SComboBox>) + .OptionsSource(&LanguageOverridesAvailable) + .OnGenerateWidget_Lambda([this](TSharedPtr InItem) -> TSharedRef + { + return SNew(STextBlock).Text(FText::FromString(*InItem)); + }) + .OnSelectionChanged_Lambda([this](TSharedPtr Item, ESelectInfo::Type SelectInfo) -> void + { + SelectedLanguageOverride = Item; + if (WidgetController.IsValid()) { - if (SelectedLanguageOverride.IsValid()) + WidgetController->OnLocalizationOnFirstRunLangChanged(*Item); + } + }) + [ + SNew(STextBlock) + .Font(FCoreStyle::GetDefaultFontStyle("Regular", 10)) + .Text_Lambda([this]() -> FText { - return FText::FromString(*SelectedLanguageOverride); - } - return FText::GetEmpty(); - }) + if (SelectedLanguageOverride.IsValid()) + { + return FText::FromString(*SelectedLanguageOverride); + } + return FText::GetEmpty(); + }) + ] ] ] ] @@ -393,93 +425,148 @@ void SELTEditorWidget::Construct(const FArguments& InArgs) +SVerticalBox::Slot() .AutoHeight() [ - SNew(SVerticalBox) - .ToolTipText(INVTEXT("A CSV column separator. It's \",\" by default, but it can be any other single character.")) - // >>>> Separator Label - +SVerticalBox::Slot() - .AutoHeight() + SNew(SBox) + .MinDesiredHeight(24.f) [ - SNew(STextBlock) - .Font(FCoreStyle::GetDefaultFontStyle("Light", 12)) - .Text(INVTEXT("Separator:")) - ] - // >>>> Separator Value - +SVerticalBox::Slot() - .AutoHeight() - .HAlign(EHorizontalAlignment::HAlign_Left) - [ - SNew(SEditableTextBox) - .Font(FCoreStyle::GetDefaultFontStyle("Regular", 12)) - .MinDesiredWidth(256.f) - .Text_Lambda([this]() -> FText - { - return FText::FromString(SeparatorValue); - }) - .OnTextCommitted_Lambda([this](const FText& NewText, ETextCommit::Type CommitType) -> void - { - SeparatorValue = NewText.ToString(); - if (WidgetController.IsValid()) + SNew(SHorizontalBox) + .ToolTipText(INVTEXT("A CSV column separator. It's \",\" by default, but it can be any other single character.")) + // >>>> Separator Label + +SHorizontalBox::Slot() + .FillWidth(1.0f) + .MaxWidth(400.0f) + .VAlign(EVerticalAlignment::VAlign_Center) + [ + SNew(STextBlock) + .Font(FCoreStyle::GetDefaultFontStyle("Light", 11)) + .Text(INVTEXT("Separator:")) + ] + // >>>> Separator Value + +SHorizontalBox::Slot() + .FillWidth(1.0f) + .MaxWidth(200.0f) + .VAlign(EVerticalAlignment::VAlign_Center) + [ + SNew(SEditableTextBox) + .Font(FCoreStyle::GetDefaultFontStyle("Regular", 11)) + .Text_Lambda([this]() -> FText { - WidgetController->OnSeparatorChanged(SeparatorValue); - } - }) + return FText::FromString(SeparatorValue); + }) + .OnTextCommitted_Lambda([this](const FText& NewText, ETextCommit::Type CommitType) -> void + { + SeparatorValue = NewText.ToString(); + if (WidgetController.IsValid()) + { + WidgetController->OnSeparatorChanged(SeparatorValue); + } + }) + ] ] ] // > Fallback when empty Box ================ +SVerticalBox::Slot() .AutoHeight() - .Padding(FMargin(0.f, 4.f, 0.f, 0.f)) [ - SNew(SHorizontalBox) - .ToolTipText(INVTEXT("\ -When the entry is empty should it fill it with a fallback value?\n\ -NONE - no fallback\n\ -FIRST_LANG - use value of the first language.If that value is empty use Key\n\ -KEY - use the key of this entry")) - // >>>> Fallback when empty Label - +SHorizontalBox::Slot() - .AutoWidth() + SNew(SBox) + .MinDesiredHeight(24.f) [ - SNew(STextBlock) - .Font(FCoreStyle::GetDefaultFontStyle("Light", 12)) - .Text(INVTEXT("Fallback when empty:")) + SNew(SHorizontalBox) + .ToolTipText(INVTEXT("\ + When the entry is empty should it fill it with a fallback value?\n\ + NONE - no fallback\n\ + FIRST_LANG - use value of the first language.If that value is empty use Key\n\ + KEY - use the key of this entry")) + // >>>> Fallback when empty Label + +SHorizontalBox::Slot() + .FillWidth(1.0f) + .MaxWidth(400.0f) + .VAlign(EVerticalAlignment::VAlign_Center) + [ + SNew(STextBlock) + .Font(FCoreStyle::GetDefaultFontStyle("Light", 11)) + .Text(INVTEXT("Fallback when empty:")) + ] + // >>>> Fallback when empty List + +SHorizontalBox::Slot() + .AutoWidth() + .VAlign(EVerticalAlignment::VAlign_Center) + [ + SNew(SComboBox>) + .OptionsSource(&FallbackWhenEmptyAvailable) + .OnGenerateWidget_Lambda([this](TSharedPtr InItem) -> TSharedRef + { + return SNew(STextBlock).Text(FText::FromString(*InItem)); + }) + .OnSelectionChanged_Lambda([this](TSharedPtr Item, ESelectInfo::Type SelectInfo) -> void + { + SelectedFallbackWhenEmpty = Item; + if (SelectedFallbackWhenEmpty.IsValid()) + { + WidgetController->OnFallbackWhenEmptyChanged(*Item); + } + }) + [ + SNew(STextBlock) + .Font(FCoreStyle::GetDefaultFontStyle("Bold", 10)) + .Text_Lambda([this]() -> FText + { + if (SelectedFallbackWhenEmpty.IsValid()) + { + return FText::FromString(*SelectedFallbackWhenEmpty); + } + return FText::GetEmpty(); + }) + ] + ] ] - // >>>> Fallback when empty List - +SHorizontalBox::Slot() - .AutoWidth() + ] + // > Generate Key Reference String Table on Import Box ================ + +SVerticalBox::Slot() + .AutoHeight() + [ + SNew(SBox) + .MinDesiredHeight(24.f) [ - SNew(SComboBox>) - .OptionsSource(&FallbackWhenEmptyAvailable) - .OnGenerateWidget_Lambda([this](TSharedPtr InItem) -> TSharedRef - { - return SNew(STextBlock).Text(FText::FromString(*InItem)); - }) - .OnSelectionChanged_Lambda([this](TSharedPtr Item, ESelectInfo::Type SelectInfo) -> void - { - SelectedFallbackWhenEmpty = Item; - if (SelectedFallbackWhenEmpty.IsValid()) - { - WidgetController->OnFallbackWhenEmptyChanged(*Item); - } - }) + SNew(SHorizontalBox) + .ToolTipText(INVTEXT("\ + On CSV Import, a String Table filled with Key References will be generated PER namespace.\n\ + These String Table can be used to easily assign keys to FText properties.\n\n\ + The String Table will be generated in the Localization Folder path and IS OVERRIDDEN if it already exists.")) + // >>>> Generate Key Reference String Table CSV Import Label + +SHorizontalBox::Slot() + .FillWidth(1.0f) + .MaxWidth(400.0f) + .VAlign(EVerticalAlignment::VAlign_Center) [ SNew(STextBlock) - .Font(FCoreStyle::GetDefaultFontStyle("Bold", 10)) - .Text_Lambda([this]() -> FText + .Font(FCoreStyle::GetDefaultFontStyle("Light", 11)) + .Text(INVTEXT("Generate Key Reference String Table on Import:")) + ] + // >>>> Generate Key Reference String Table on Import checkbox + +SHorizontalBox::Slot() + .AutoWidth() + .VAlign(EVerticalAlignment::VAlign_Center) + [ + SNew(SCheckBox) + .IsChecked_Lambda([this]() -> ECheckBoxState + { + return bGenerateKeyReferenceStringTable_Chkbox ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }) + .OnCheckStateChanged_Lambda([this](ECheckBoxState InCheckBoxState) -> void + { + bGenerateKeyReferenceStringTable_Chkbox = (InCheckBoxState == ECheckBoxState::Checked); + if (WidgetController.IsValid()) { - if (SelectedFallbackWhenEmpty.IsValid()) - { - return FText::FromString(*SelectedFallbackWhenEmpty); - } - return FText::GetEmpty(); - }) + WidgetController->OnGenerateKeyReferenceStringTableChanged(bGenerateKeyReferenceStringTable_Chkbox); + } + }) ] ] ] // > Spacer ================ +SVerticalBox::Slot() .AutoHeight() - .Padding(FMargin(0.f, 4.f, 0.f, 4.f)) + .Padding(FMargin(0.f, 15.f, 0.f, 15.f)) .HAlign(EHorizontalAlignment::HAlign_Left) .VAlign(EVerticalAlignment::VAlign_Center) [ @@ -490,7 +577,7 @@ KEY - use the key of this entry")) .AutoHeight() [ SNew(SVerticalBox) - .ToolTipText(INVTEXT("CSV files to import. You can import mutliple files at once to the same Localization.")) + .ToolTipText(INVTEXT("CSV files to import. You can import multiple files at once to the same Localization.")) // >>>> CSV files list box +SVerticalBox::Slot() .AutoHeight() @@ -501,7 +588,7 @@ KEY - use the key of this entry")) .AutoHeight() [ SNew(STextBlock) - .Font(FCoreStyle::GetDefaultFontStyle("Light", 12)) + .Font(FCoreStyle::GetDefaultFontStyle("Light", 11)) .Text(INVTEXT("CSV file:")) ] // >>>>>>> CSV files list value @@ -509,7 +596,7 @@ KEY - use the key of this entry")) .AutoHeight() [ SNew(STextBlock) - .Font(FCoreStyle::GetDefaultFontStyle("Bold", 12)) + .Font(FCoreStyle::GetDefaultFontStyle("Bold", 11)) .Text_Lambda([this]() -> FText { return FText::FromString(CSVFiles); @@ -519,6 +606,7 @@ KEY - use the key of this entry")) +SVerticalBox::Slot() // >>>> CSV select files box .AutoHeight() + .Padding(FMargin(0.f, 5.f, 0.f, 0.f)) [ SNew(SHorizontalBox) // >>>>>>> CSV select files button @@ -556,43 +644,51 @@ KEY - use the key of this entry")) // > Global namespace box ================ +SVerticalBox::Slot() .AutoHeight() + .Padding(FMargin(0.f, 15.f, 0.f, 0.f)) [ - SNew(SVerticalBox) - .ToolTipText(INVTEXT("This namespace will be assigned to every key in localization.")) - // >>>> Global namespace label ================ - +SVerticalBox::Slot() - .AutoHeight() + SNew(SBox) + .MinDesiredHeight(24.f) [ - SNew(STextBlock) - .Font(FCoreStyle::GetDefaultFontStyle("Light", 12)) - .Text(INVTEXT("Global namespace:")) - ] - // >>>> Global namespace value ================ - +SVerticalBox::Slot() - .AutoHeight() - .HAlign(EHorizontalAlignment::HAlign_Left) - [ - SNew(SEditableTextBox) - .Font(FCoreStyle::GetDefaultFontStyle("Regular", 12)) - .MinDesiredWidth(256.f) - .Text_Lambda([this]() -> FText - { - return FText::FromString(GlobalNamespaceValue); - }) - .OnTextCommitted_Lambda([this](const FText& NewText, ETextCommit::Type CommitType) -> void - { - GlobalNamespaceValue = NewText.ToString(); - if (WidgetController.IsValid()) + SNew(SHorizontalBox) + .ToolTipText(INVTEXT("This namespace will be assigned to every key in localization.")) + // >>>> Global namespace label ================ + +SHorizontalBox::Slot() + .FillWidth(1.0f) + .MaxWidth(400.0f) + .VAlign(EVerticalAlignment::VAlign_Center) + [ + SNew(STextBlock) + .Font(FCoreStyle::GetDefaultFontStyle("Light", 11)) + .Text(INVTEXT("Global namespace:")) + ] + // >>>> Global namespace value ================ + +SHorizontalBox::Slot() + .FillWidth(1.0f) + .MaxWidth(200.0f) + .VAlign(EVerticalAlignment::VAlign_Center) + [ + SNew(SEditableTextBox) + .Font(FCoreStyle::GetDefaultFontStyle("Regular", 11)) + .MinDesiredWidth(256.f) + .Text_Lambda([this]() -> FText { - WidgetController->OnGlobalNamespaceChanged(GlobalNamespaceValue); - } - }) + return FText::FromString(GlobalNamespaceValue); + }) + .OnTextCommitted_Lambda([this](const FText& NewText, ETextCommit::Type CommitType) -> void + { + GlobalNamespaceValue = NewText.ToString(); + if (WidgetController.IsValid()) + { + WidgetController->OnGlobalNamespaceChanged(GlobalNamespaceValue); + } + }) + ] ] ] // > Spacer ================ +SVerticalBox::Slot() .AutoHeight() - .Padding(FMargin(0.f, 4.f, 0.f, 4.f)) + .Padding(FMargin(0.f, 15.f, 0.f, 15.f)) .HAlign(EHorizontalAlignment::HAlign_Left) .VAlign(EVerticalAlignment::VAlign_Center) [ @@ -602,77 +698,89 @@ KEY - use the key of this entry")) +SVerticalBox::Slot() .AutoHeight() [ - SNew(SHorizontalBox) - .ToolTipText(INVTEXT("Select this option to see additional informations in Output Log.\nBe aware that big CSVs might generate a lot of logs.")) - // >>>> Log Debug Label - +SHorizontalBox::Slot() - .AutoWidth() + SNew(SBox) + .MinDesiredHeight(24.f) [ - SNew(STextBlock) - .Font(FCoreStyle::GetDefaultFontStyle("Light", 12)) - .Text(INVTEXT("Log Debug:")) - ] - // >>>> Log Debug checkbox - +SHorizontalBox::Slot() - .AutoWidth() - [ - SNew(SCheckBox) - .IsChecked_Lambda([this]() -> ECheckBoxState - { - return bLogDebug_Chkbox ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; - }) - .OnCheckStateChanged_Lambda([this](ECheckBoxState InCheckBoxState) -> void - { - bLogDebug_Chkbox = (InCheckBoxState == ECheckBoxState::Checked); - if (WidgetController.IsValid()) + SNew(SHorizontalBox) + .ToolTipText(INVTEXT("Select this option to see additional informations in Output Log.\nBe aware that big CSVs might generate a lot of logs.")) + // >>>> Log Debug Label + +SHorizontalBox::Slot() + .MaxWidth(400.0f) + .VAlign(EVerticalAlignment::VAlign_Center) + [ + SNew(STextBlock) + .Font(FCoreStyle::GetDefaultFontStyle("Light", 11)) + .Text(INVTEXT("Log Debug:")) + ] + // >>>> Log Debug checkbox + +SHorizontalBox::Slot() + .AutoWidth() + .VAlign(EVerticalAlignment::VAlign_Center) + [ + SNew(SCheckBox) + .IsChecked_Lambda([this]() -> ECheckBoxState { - WidgetController->OnLogDebugChanged(bLogDebug_Chkbox); - } - }) + return bLogDebug_Chkbox ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }) + .OnCheckStateChanged_Lambda([this](ECheckBoxState InCheckBoxState) -> void + { + bLogDebug_Chkbox = (InCheckBoxState == ECheckBoxState::Checked); + if (WidgetController.IsValid()) + { + WidgetController->OnLogDebugChanged(bLogDebug_Chkbox); + } + }) + ] ] ] // > Preview In UI Box ================ +SVerticalBox::Slot() .AutoHeight() [ - SNew(SHorizontalBox) - .ToolTipText(INVTEXT("Select this option to show a localization preview under the Text fields in the Editor UI.")) - .Visibility_Lambda([this]() -> EVisibility - { - if (WidgetController.IsValid()) - { - if (WidgetController->IsPreviewInUISupported()) - { - return EVisibility::Visible; - } - } - return EVisibility::Collapsed; - }) - // >>>> Preview In UI Label - +SHorizontalBox::Slot() - .AutoWidth() + SNew(SBox) + .MinDesiredHeight(24.f) [ - SNew(STextBlock) - .Font(FCoreStyle::GetDefaultFontStyle("Light", 12)) - .Text(INVTEXT("Show preview in UI:")) - ] - // >>>> Preview In UI checkbox - +SHorizontalBox::Slot() - .AutoWidth() - [ - SNew(SCheckBox) - .IsChecked_Lambda([this]() -> ECheckBoxState - { - return bPreviewInUI_Chkbox ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; - }) - .OnCheckStateChanged_Lambda([this](ECheckBoxState InCheckBoxState) -> void + SNew(SHorizontalBox) + .ToolTipText(INVTEXT("Select this option to show a localization preview under the Text fields in the Editor UI.")) + .Visibility_Lambda([this]() -> EVisibility + { + if (WidgetController.IsValid()) { - bPreviewInUI_Chkbox = (InCheckBoxState == ECheckBoxState::Checked); - if (WidgetController.IsValid()) + if (WidgetController->IsPreviewInUISupported()) { - WidgetController->OnPreviewInUIChanged(bPreviewInUI_Chkbox); + return EVisibility::Visible; } - }) + } + return EVisibility::Collapsed; + }) + // >>>> Preview In UI Label + +SHorizontalBox::Slot() + .MaxWidth(400.0f) + .VAlign(EVerticalAlignment::VAlign_Center) + [ + SNew(STextBlock) + .Font(FCoreStyle::GetDefaultFontStyle("Light", 11)) + .Text(INVTEXT("Show preview in UI:")) + ] + // >>>> Preview In UI checkbox + +SHorizontalBox::Slot() + .AutoWidth() + .VAlign(EVerticalAlignment::VAlign_Center) + [ + SNew(SCheckBox) + .IsChecked_Lambda([this]() -> ECheckBoxState + { + return bPreviewInUI_Chkbox ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + }) + .OnCheckStateChanged_Lambda([this](ECheckBoxState InCheckBoxState) -> void + { + bPreviewInUI_Chkbox = (InCheckBoxState == ECheckBoxState::Checked); + if (WidgetController.IsValid()) + { + WidgetController->OnPreviewInUIChanged(bPreviewInUI_Chkbox); + } + }) + ] ] ] ] @@ -819,6 +927,11 @@ void SELTEditorWidget::SetFallbackWhenEmpty(const FString& FallbackWhenEmpty) } } +void SELTEditorWidget::SetGenerateKeyReferenceStringTable(bool bGenerateKeyReferenceStringTable) +{ + bGenerateKeyReferenceStringTable_Chkbox = bGenerateKeyReferenceStringTable; +} + void SELTEditorWidget::SetLogDebug(bool bLogDebug) { bLogDebug_Chkbox = bLogDebug; diff --git a/Source/EasyLocalizationToolEditor/Public/ELTEditor.h b/Source/EasyLocalizationToolEditor/Public/ELTEditor.h index 9e2668b..ee6e9ae 100644 --- a/Source/EasyLocalizationToolEditor/Public/ELTEditor.h +++ b/Source/EasyLocalizationToolEditor/Public/ELTEditor.h @@ -49,8 +49,8 @@ class EASYLOCALIZATIONTOOLEDITOR_API UELTEditor : public UObject * Implementation of generating Unreal localization files. It is statically exposed, * so other elements like Commandlet can run it. */ - static bool GenerateLocFilesImpl(const FString& CSVPaths, const FString& LocPath, const FString& LocName, const FString& GlobalNamespace, const FString& Separator, const FString& FallbackWhenEmpty, FString& OutMessage); - static bool GenerateLocFilesImpl(const TArray& CSVPaths, const FString& LocPath, const FString& LocName, const FString& GlobalNamespace, const FString& Separator, const FString& FallbackWhenEmpty, FString& OutMessage); + static bool GenerateLocFilesImpl(const FString& CSVPaths, const FString& LocPath, const FString& LocName, const FString& GlobalNamespace, const FString& Separator, const FString& FallbackWhenEmpty, bool bGenerateStringTables, FString& OutMessage); + static bool GenerateLocFilesImpl(const TArray& CSVPaths, const FString& LocPath, const FString& LocName, const FString& GlobalNamespace, const FString& Separator, const FString& FallbackWhenEmpty, bool bGenerateStringTables, FString& OutMessage); private: @@ -144,6 +144,11 @@ class EASYLOCALIZATIONTOOLEDITOR_API UELTEditor : public UObject */ void OnFallbackWhenEmptyChanged(const FString& NewFallback); + /** + * Called when "GenerateKeyReferenceStringTable" option has been changed in the Widget. + */ + void OnGenerateKeyReferenceStringTableChanged(bool bNewGenerateKeyReferenceStringTable); + /** * Called when "LogDebug" option has been changed in the Widget. */ @@ -162,11 +167,23 @@ class EASYLOCALIZATIONTOOLEDITOR_API UELTEditor : public UObject */ void SetLanguagePreview(); + /** + * Controls which UI elements are updated when RefreshAvailableLangs is called. + */ + enum class ERefreshUIFlags : uint8 + { + None = 0, + ToolWidget = 1 << 0, + AuditWidget = 1 << 1, + All = ToolWidget | AuditWidget, + }; + FRIEND_ENUM_CLASS_FLAGS(ERefreshUIFlags); + /** * Refresh the list of available languages based on the files that exists in Localization directory. - * It can optionally RefreshUI. + * UIFlags controls which widgets are updated */ - void RefreshAvailableLangs(bool bRefreshUI); + void RefreshAvailableLangs(ERefreshUIFlags UIFlags); /** * Generates Localization Files based on the given CSV path and Global Namespace. @@ -225,4 +242,4 @@ class EASYLOCALIZATIONTOOLEDITOR_API UELTEditor : public UObject // Cache of the CSV Paths defined for each Localization directory. TMap CSVPaths; -}; +}; \ No newline at end of file diff --git a/Source/EasyLocalizationToolEditor/Public/ELTEditorAuditTypes.h b/Source/EasyLocalizationToolEditor/Public/ELTEditorAuditTypes.h new file mode 100644 index 0000000..6c8c47b --- /dev/null +++ b/Source/EasyLocalizationToolEditor/Public/ELTEditorAuditTypes.h @@ -0,0 +1,150 @@ +// Copyright (c) 2026 Crezetique. All rights reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "ELTEditorAuditTypes.generated.h" + +UENUM(BlueprintType) +enum class EELTAuditParameterType : uint8 +{ + // CDO-level variable or Blueprint class variable. + ClassVariable UMETA(DisplayName = "Class Variable"), + + // Local variable or input/output parameter on a Blueprint function graph. + FunctionVariable UMETA(DisplayName = "Function Variable"), + + // Inline literal FText pin on a Blueprint graph node. + NodeParameter UMETA(DisplayName = "Node Parameter"), + + // FText property on a UMG widget component inside a Widget Blueprint's widget tree. + WidgetComponent UMETA(DisplayName = "Widget Component"), +}; + +UENUM(BlueprintType) +enum class EELTAuditIssueType : uint8 +{ + // FText passed the audit — no localization issue detected. + None UMETA(DisplayName = "-"), + + // FText field is empty. + EmptyValue UMETA(DisplayName = "Empty Value"), + + // String Table reference is broken — table missing from registry or key absent from it. + StringTableMissingKey UMETA(DisplayName = "String Table Missing Key"), + + // Key does not match the source value — text was never set up via ELT or was edited after. + NotYetLocalized UMETA(DisplayName = "Not Yet Localized"), + + // Key matches source but is absent from the live localization table. + InvalidLocalization UMETA(DisplayName = "Invalid Localization"), +}; + +// One row in the audit results table. +USTRUCT(BlueprintType) +struct FELTAuditIssue +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadOnly, Category = "ELT Audit") + FString AssetName; + + UPROPERTY(BlueprintReadOnly, Category = "ELT Audit") + EELTAuditParameterType Type = EELTAuditParameterType::ClassVariable; + + // Variable name, pin name, or node display name depending on Type. + UPROPERTY(BlueprintReadOnly, Category = "ELT Audit") + FString VariableNodeName; + + UPROPERTY(BlueprintReadOnly, Category = "ELT Audit") + EELTAuditIssueType Issue = EELTAuditIssueType::None; + + UPROPERTY(BlueprintReadOnly, Category = "ELT Audit") + FString Value; + + // The resolved localization string for the current language at the time of audit. + UPROPERTY(BlueprintReadOnly, Category = "ELT Audit") + FString LocalizedString; + + UPROPERTY(BlueprintReadOnly, Category = "ELT Audit") + FString Key; + + UPROPERTY(BlueprintReadOnly, Category = "ELT Audit") + FString Namespace; + + // True when the FText is backed by a String Table reference. + UPROPERTY(BlueprintReadOnly, Category = "ELT Audit") + bool bIsUsingStringTable = false; + + // Navigation data used by the Quick Action column — not displayed as table columns. + UPROPERTY(BlueprintReadOnly, Category = "ELT Audit") + FGuid NodeGuid; + + UPROPERTY(BlueprintReadOnly, Category = "ELT Audit") + FString GraphName; + + bool HasIssue() const { return Issue != EELTAuditIssueType::None; } + bool IsEmpty() const { return Issue == EELTAuditIssueType::EmptyValue; } + + FString TypeDisplayString() const + { + switch (Type) + { + case EELTAuditParameterType::ClassVariable: return TEXT("Class Variable"); + case EELTAuditParameterType::FunctionVariable: return TEXT("Function Variable"); + case EELTAuditParameterType::NodeParameter: return TEXT("Node Parameter"); + case EELTAuditParameterType::WidgetComponent: return TEXT("Widget Component"); + } + return TEXT(""); + } + + FString IssueDisplayString() const + { + switch (Issue) + { + case EELTAuditIssueType::None: return TEXT("-"); + case EELTAuditIssueType::EmptyValue: return TEXT("Empty Value"); + case EELTAuditIssueType::StringTableMissingKey: return TEXT("String Table Missing Key"); + case EELTAuditIssueType::NotYetLocalized: return TEXT("Not Yet Localized"); + case EELTAuditIssueType::InvalidLocalization: return TEXT("Invalid Localization"); + } + return TEXT(""); + } + + FString IssueTooltipString() const + { + switch (Issue) + { + case EELTAuditIssueType::None: return TEXT(""); + case EELTAuditIssueType::EmptyValue: return TEXT("This FText field has no value set. \n\nIf this is intentional for state or function processing purposes, it is recommended to toggle FText localize bool to false."); + case EELTAuditIssueType::StringTableMissingKey: return TEXT("String Table is set, but an invalid key is selected."); + case EELTAuditIssueType::NotYetLocalized: return TEXT("Unable to fetch a localization string. (Value and Key do not match)"); + case EELTAuditIssueType::InvalidLocalization: return TEXT("The localization key does not return a valid localization string."); + } + return TEXT(""); + } + + FString ToLogString() const + { + return FString::Printf(TEXT("[%s] [%s] %s Value='%s' NS='%s' Key='%s'"), + *TypeDisplayString(), *IssueDisplayString(), *VariableNodeName, *Value, *Namespace, *Key); + } +}; + +// Aggregates all issues found for a single asset. The widget flattens these into one list. +USTRUCT(BlueprintType) +struct FELTAssetAuditResult +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadOnly, Category = "ELT Audit") + FAssetData AssetData; + + UPROPERTY(BlueprintReadOnly, Category = "ELT Audit") + TArray Issues; + + bool HasIssues() const + { + return Issues.ContainsByPredicate([](const FELTAuditIssue& I){ return I.HasIssue(); }); + } +}; \ No newline at end of file diff --git a/Source/EasyLocalizationToolEditor/Public/ELTEditorAuditWidget.h b/Source/EasyLocalizationToolEditor/Public/ELTEditorAuditWidget.h new file mode 100644 index 0000000..05f9f38 --- /dev/null +++ b/Source/EasyLocalizationToolEditor/Public/ELTEditorAuditWidget.h @@ -0,0 +1,130 @@ +// Copyright (c) 2026 Crezetique. All rights reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "ELTEditorAuditTypes.h" +#include "Widgets/SCompoundWidget.h" +#include "Widgets/Views/SListView.h" + +class STableViewBase; +class ITableRow; +class SDockTab; + +class SELTEditorAuditWidget : public SCompoundWidget +{ +public: + SLATE_BEGIN_ARGS(SELTEditorAuditWidget) {} + SLATE_ARGUMENT(TArray, AuditResults) + SLATE_END_ARGS() + + void Construct(const FArguments& InArgs); + + void Refresh(const TArray& InResults); + void RefreshLanguages(const TArray& Languages); + void SetPendingCompletionDialog(bool bPending) { bPendingCompletionDialog = bPending; } + + static void JumpToIssue(const FAssetData& AssetData, const FELTAuditIssue& Issue); + +private: + using FIssuePtr = TSharedPtr; + using FLanguagePtr = TSharedPtr; + + // Full unfiltered list + TArray AllIssues; + + // Filtered + Sorted view + TArray FlatIssues; + + // Key AssetName, OnGenerateIssueRow. + TMap AssetDataMap; + + // All asset data from the last audit run, used to reaudit via Refresh(). + TArray LastAuditedAssets; + + // Widget Pointers + TSharedPtr SummaryText; + TSharedPtr TipText; + TSharedPtr AuditHeaderRow; + TSharedPtr> ListView; + + // Language Dropdown + TArray AvailableLanguages; + TSharedPtr> LanguageComboBox; + FLanguagePtr SelectedLanguage; + + // Hide rows with no issue + bool bFilterIssues = false; + + // Hide rows with empty text values + bool bHideEmpty = false; + + // Shows more details in list + bool bShowMoreDetails = true; + + // When true, Refresh() will show the audit completion dialog after updating the widget. + bool bPendingCompletionDialog = false; + + // More Detail columns + static const TArray& GetDetailColumns() + { + static const TArray Cols = { + TEXT("LocalizedString"), + TEXT("Value"), + TEXT("Key"), + TEXT("Namespace"), + TEXT("IsUsingStringTable") + }; + return Cols; + } + + FName SortColumn; + EColumnSortMode::Type SortMode = EColumnSortMode::None; + + void RebuildAuditData(const TArray& InResults); + FText BuildSummaryText() const; + FText PickRandomTip() const; + + TSharedRef OnGenerateIssueRow(FIssuePtr Issue, const TSharedRef& OwnerTable); + void OnSortColumn(EColumnSortPriority::Type Priority, const FName& Column, EColumnSortMode::Type Mode); + void OnFilterIssuesChanged(ECheckBoxState NewState); + void OnHideEmptyChanged(ECheckBoxState NewState); + void OnShowMoreDetailsChanged(ECheckBoxState NewState); + void OnLanguageSelected(FLanguagePtr Item, ESelectInfo::Type SelectInfo); + TSharedRef OnGenerateLanguageComboRow(FLanguagePtr Item); + FReply OnRefreshAuditClicked(); + FReply OnReimportCSVClicked(); + + void ApplySort(); + void ApplyFilter(); + void ApplyDetailColumnVisibility(); + EColumnSortMode::Type GetSortModeForColumn(FName Column) const; +}; + +DECLARE_DELEGATE(FOnAuditWidgetReimportCSV); + +class EASYLOCALIZATIONTOOLEDITOR_API UELTEditorAuditWidget +{ +public: + static void ShowResults(const TArray& Results); + + // Called after a CSV reimport to refresh the language dropdown with newly available languages. + static void UpdateAvailableLanguages(const TArray& Languages); + + // Shows the audit complete dialog. Called from Refresh() once the widget is live. + static void ShowCompletionDialog(const TArray& Results); + + static const FName TabName; + + // Bound by UELTEditor to trigger CSV reimport when the Reimport CSV button is clicked. + static FOnAuditWidgetReimportCSV OnReimportCSVDelegate; + + // Set before SpawnTab is called so Construct can read it synchronously. + static bool bPendingDialogOnSpawn; + +private: + static TSharedRef SpawnTab(const FSpawnTabArgs& Args); + + static TArray PendingResults; + static TWeakPtr LiveWidget; +}; \ No newline at end of file diff --git a/Source/EasyLocalizationToolEditor/Public/ELTEditorAuditor.h b/Source/EasyLocalizationToolEditor/Public/ELTEditorAuditor.h new file mode 100644 index 0000000..4540251 --- /dev/null +++ b/Source/EasyLocalizationToolEditor/Public/ELTEditorAuditor.h @@ -0,0 +1,43 @@ +// Copyright (c) 2026 Crezetique. All rights reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "EditorUtilityObject.h" +#include "Engine/StreamableManager.h" +#include "ELTEditorAuditTypes.h" +#include "ELTEditorAuditor.generated.h" + +class UWidgetBlueprint; + +UCLASS() +class EASYLOCALIZATIONTOOLEDITOR_API UELTEditorAuditor : public UEditorUtilityObject +{ + GENERATED_BODY() + +public: + UFUNCTION(BlueprintCallable, CallInEditor, Category = "ELT|Localization Audit") + void AuditSelectedAssets(); + + UFUNCTION(BlueprintCallable, Category = "ELT|Localization Audit") + void AuditAssets(const TArray& Assets); + + static TArray RunAudit(const TArray& Assets); + + // Held alive until async asset loading completes before running the audit. + static TSharedPtr PendingLoadHandle; + +private: + static void AuditSingleAsset(UObject* Asset, const FAssetData& AssetData, FELTAssetAuditResult& OutResult); + + // Audits every FText reachable through the CDO property tree. This will recursively run through base classes and relevant properties. + static void AuditClassProperties(void* ContainerPtr, UStruct* Struct, const FString& PathPrefix, FELTAssetAuditResult& OutResult); + + // Audits Blueprint class variables, function local variables, function parameter pins, and inline node pins. + static void AuditBlueprintTextSources(UBlueprint* Blueprint, FELTAssetAuditResult& OutResult); + + // Audits FText properties on UMG widget components inside a Widget Blueprint's widget tree. + static void AuditWidgetComponents(UWidgetBlueprint* WidgetBlueprint, FELTAssetAuditResult& OutResult); + + static void AuditText(const FText& InText, EELTAuditParameterType ParameterType, const FString& VariableNodeName, const FGuid& NodeGuid, const FString& GraphName, FELTAssetAuditResult& OutResult); +}; \ No newline at end of file diff --git a/Source/EasyLocalizationToolEditor/Public/ELTEditorCommands.h b/Source/EasyLocalizationToolEditor/Public/ELTEditorCommands.h index 18d558c..3c3c06f 100644 --- a/Source/EasyLocalizationToolEditor/Public/ELTEditorCommands.h +++ b/Source/EasyLocalizationToolEditor/Public/ELTEditorCommands.h @@ -20,4 +20,5 @@ class EASYLOCALIZATIONTOOLEDITOR_API FELTEditorCommands : public TCommands OpenELTMenu; + TSharedPtr RunELTAudit; }; diff --git a/Source/EasyLocalizationToolEditor/Public/ELTEditorSettings.h b/Source/EasyLocalizationToolEditor/Public/ELTEditorSettings.h index dcd8da2..e748c96 100644 --- a/Source/EasyLocalizationToolEditor/Public/ELTEditorSettings.h +++ b/Source/EasyLocalizationToolEditor/Public/ELTEditorSettings.h @@ -59,6 +59,9 @@ class EASYLOCALIZATIONTOOLEDITOR_API UELTEditorSettings : public UObject static bool GetReimportAtEditorStartup(); static void SetReimportAtEditorStartup(bool bNewReimportAtEditorStartup); + static bool GetGenerateKeyReferenceStringTable(); + static void SetGenerateKeyReferenceStringTable(bool bNewGenerateKeyReferenceStringTable); + /** * Get/Set if the preview should be displayed on UI. */ @@ -99,7 +102,10 @@ class EASYLOCALIZATIONTOOLEDITOR_API UELTEditorSettings : public UObject UPROPERTY(config) bool bReimportAtEditorStartup = false; - + + UPROPERTY(config) + bool bGenerateKeyReferenceStringTable = false; + UPROPERTY(config) bool bPreviewInUI = true; diff --git a/Source/EasyLocalizationToolEditor/Public/ELTEditorWidget.h b/Source/EasyLocalizationToolEditor/Public/ELTEditorWidget.h index 3853c5a..dc5b79e 100644 --- a/Source/EasyLocalizationToolEditor/Public/ELTEditorWidget.h +++ b/Source/EasyLocalizationToolEditor/Public/ELTEditorWidget.h @@ -23,6 +23,7 @@ DECLARE_DELEGATE_OneParam(FOnLocalizationPreviewLangChanged, const FString&); DECLARE_DELEGATE_OneParam(FManuallySetLastLanguageChanged, bool); DECLARE_DELEGATE_OneParam(FOnLocalizationOnFirstRunChanged, bool); DECLARE_DELEGATE_OneParam(FOnLocalizationOnFirstRunLangChanged, const FString&); +DECLARE_DELEGATE_OneParam(FOnGenerateKeyReferenceStringTableChanged, bool); DECLARE_DELEGATE_OneParam(FOnGlobalNamespaceChanged, const FString&); DECLARE_DELEGATE_OneParam(FOnSeparatorChanged, const FString&); DECLARE_DELEGATE_OneParam(FOnFallbackWhenEmptyChanged, const FString&); @@ -196,6 +197,18 @@ class EASYLOCALIZATIONTOOLEDITOR_API UELTEditorWidget : public UEditorUtilityWid + /** + * Set "Generate Key Reference String Table On CSV Import" to the Widget. + */ + void CallSetGenerateKeyReferenceStringTable(bool bGenerateKeyReferenceStringTable); + + /** + * "Generate Key Reference String Table On CSV Import" option has been changed on the Widget. + */ + UFUNCTION(BlueprintCallable, Category = "Easy Localization Tool Editor") + void OnGenerateKeyReferenceStringTableChanged(bool bGenerateKeyReferenceStringTable); + + /** * Set "Global Namespace" option to the Widget. */ @@ -287,6 +300,7 @@ class EASYLOCALIZATIONTOOLEDITOR_API UELTEditorWidget : public UEditorUtilityWid FManuallySetLastLanguageChanged OnManuallySetLastLanguageChangedDelegate; FOnLocalizationOnFirstRunChanged OnLocalizationOnFirstRunChangedDelegate; FOnLocalizationOnFirstRunLangChanged OnLocalizationOnFirstRunLangChangedDelegate; + FOnGenerateKeyReferenceStringTableChanged OnGenerateKeyReferenceStringTableChangedDelegate; FOnGlobalNamespaceChanged OnGlobalNamespaceChangedDelegate; FOnSeparatorChanged OnSeparatorChangedDelegate; FOnFallbackWhenEmptyChanged OnFallbackWhenEmptyChangedDelegate; diff --git a/Source/EasyLocalizationToolEditor/Public/EasyLocalizationToolEditorModule.h b/Source/EasyLocalizationToolEditor/Public/EasyLocalizationToolEditorModule.h index 74771ba..c54dec0 100644 --- a/Source/EasyLocalizationToolEditor/Public/EasyLocalizationToolEditorModule.h +++ b/Source/EasyLocalizationToolEditor/Public/EasyLocalizationToolEditorModule.h @@ -54,6 +54,11 @@ class EASYLOCALIZATIONTOOLEDITOR_API FEasyLocalizationToolEditorModule : public * Invokes spawning editor from the command. */ void InvokeEditorSpawn(); + + /** + * Audit selected assets if content browser is focused, otherwise audit focused editor window if it is an asset. + */ + void RunAuditCommand(); // Editor object. #if (ENGINE_MAJOR_VERSION == 5) @@ -69,4 +74,7 @@ class EASYLOCALIZATIONTOOLEDITOR_API FEasyLocalizationToolEditorModule : public FDelegateHandle OnPostEngineInitDelegateHandle; TSharedPtr GraphPanelPinFactory; -}; + + FDelegateHandle ContentBrowserExtenderHandle; + FDelegateHandle PathViewExtenderHandle; +}; \ No newline at end of file diff --git a/Source/EasyLocalizationToolEditor/Public/SELTEditorWidget.h b/Source/EasyLocalizationToolEditor/Public/SELTEditorWidget.h index 58f6487..cdcf909 100644 --- a/Source/EasyLocalizationToolEditor/Public/SELTEditorWidget.h +++ b/Source/EasyLocalizationToolEditor/Public/SELTEditorWidget.h @@ -33,6 +33,7 @@ class EASYLOCALIZATIONTOOLEDITOR_API SELTEditorWidget : public SUserWidget void SetGlobalNamespace(const FString& GlobalNamespace); void SetSeparator(const FString& Separator); void SetFallbackWhenEmpty(const FString& FallbackWhenEmpty); + void SetGenerateKeyReferenceStringTable(bool bGenerateKeyReferenceStringTable); void SetLogDebug(bool bLogDebug); void SetPreviewInUI(bool bPreviewInUI); @@ -54,6 +55,7 @@ class EASYLOCALIZATIONTOOLEDITOR_API SELTEditorWidget : public SUserWidget bool bIsLocalisationPreviewEnabled_Chkbox = false; bool bManuallySetLastLanguage_Chkbox = false; bool bOverrideLanguageOnStartup_Chkbox = false; + bool bGenerateKeyReferenceStringTable_Chkbox = false; bool bLogDebug_Chkbox = false; bool bPreviewInUI_Chkbox = false;