// Copyright Epic Games, Inc. All Rights Reserved. #include "Commandlets/StabilizeLocalizationKeys.h" #include "HAL/FileManager.h" #include "Misc/Paths.h" #include "Misc/Guid.h" #include "UObject/UObjectHash.h" #include "Serialization/ArchiveUObject.h" #include "Internationalization/TextNamespaceUtil.h" #include "Misc/PackageName.h" #include "UObject/PackageFileSummary.h" #include "UObject/Linker.h" #include "Internationalization/InternationalizationManifest.h" #include "Internationalization/InternationalizationArchive.h" #include "UserDefinedStructure/UserDefinedStructEditorData.h" #include "Engine/UserDefinedStruct.h" #include "Serialization/JsonInternationalizationManifestSerializer.h" #include "Serialization/JsonInternationalizationArchiveSerializer.h" #include "Internationalization/TextPackageNamespaceUtil.h" #include "Templates/UniquePtr.h" #include "LocalizedAssetUtil.h" #include "LocalizationSourceControlUtil.h" DEFINE_LOG_CATEGORY_STATIC(LogStabilizeLocalizationKeys, Log, All); #if USE_STABLE_LOCALIZATION_KEYS struct FLocTextIdentity { public: FLocTextIdentity(FString InNamespace, FString InKey) : Namespace(MoveTemp(InNamespace)), Key(MoveTemp(InKey)), Hash(0) { Hash = FCrc::StrCrc32(*Namespace, Hash); Hash = FCrc::StrCrc32(*Key, Hash); } FORCEINLINE const FString& GetNamespace() const { return Namespace; } FORCEINLINE const FString& GetKey() const { return Key; } FORCEINLINE bool operator==(const FLocTextIdentity& Other) const { return Namespace.Equals(Other.Namespace, ESearchCase::CaseSensitive) && Key.Equals(Other.Key, ESearchCase::CaseSensitive); } FORCEINLINE bool operator!=(const FLocTextIdentity& Other) const { return !Namespace.Equals(Other.Namespace, ESearchCase::CaseSensitive) || !Key.Equals(Other.Key, ESearchCase::CaseSensitive); } friend inline uint32 GetTypeHash(const FLocTextIdentity& Id) { return Id.Hash; } private: FString Namespace; FString Key; uint32 Hash; }; class FTextKeyingArchive: public FArchiveUObject { public: FTextKeyingArchive(UPackage* InPackage, TMap& InOutPackageTextKeyMap) : PackageTextKeyMap(&InOutPackageTextKeyMap), PackageNamespace(TextNamespaceUtil::EnsurePackageNamespace(InPackage)) { this->SetIsSaving(true); TArray AllObjectsInPackage; GetObjectsWithOuter(InPackage, AllObjectsInPackage, true, RF_Transient, EInternalObjectFlags::PendingKill); for (UObject* Obj: AllObjectsInPackage) { ProcessObject(Obj); } } void ProcessObject(UObject* Obj) { // User Defined Structs need some special handling as they store their default data in a way that serialize doesn't pick up if (UUserDefinedStruct* const UserDefinedStruct = Cast(Obj)) { if (UUserDefinedStructEditorData* UDSEditorData = Cast(UserDefinedStruct->EditorData)) { for (FStructVariableDescription& StructVariableDesc: UDSEditorData->VariablesDescriptions) { static const FName TextCategory = TEXT("text"); // Must match UEdGraphSchema_K2::PC_Text if (StructVariableDesc.Category == TextCategory) { FText StructVariableValue; if (FTextStringHelper::ReadFromBuffer(*StructVariableDesc.DefaultValue, StructVariableValue) && KeyText(StructVariableValue)) { FTextStringHelper::WriteToBuffer(StructVariableDesc.DefaultValue, StructVariableValue); } } } } } Obj->Serialize(*this); } bool KeyText(FText& InOutText) { if (!InOutText.ShouldGatherForLocalization()) { return false; } FString Namespace; FString Key; const bool bFoundNamespaceAndKey = FTextLocalizationManager::Get().FindNamespaceAndKeyFromDisplayString(FTextInspector::GetSharedDisplayString(InOutText), Namespace, Key); if (!bFoundNamespaceAndKey) { return false; } const FString CurrentPackageNamespace = TextNamespaceUtil::ExtractPackageNamespace(Namespace); if (CurrentPackageNamespace.Equals(PackageNamespace, ESearchCase::CaseSensitive)) { return false; } FString NewNamespace; FString NewKey; const FLocTextIdentity* ExistingMapping = PackageTextKeyMap->Find(FLocTextIdentity(Namespace, Key)); if (ExistingMapping) { NewNamespace = ExistingMapping->GetNamespace(); NewKey = ExistingMapping->GetKey(); } else { // We only want to stabilize actual asset content - these all have GUID based keys, as prior to stable keys, you could never set a non-GUID based key in an asset (it must have come from C++) { FGuid KeyGuid; if (!FGuid::Parse(Key, KeyGuid)) { return false; } } NewNamespace = TextNamespaceUtil::BuildFullNamespace(Namespace, PackageNamespace, /*bAlwaysApplyPackageNamespace*/ true); NewKey = Key; PackageTextKeyMap->Add(FLocTextIdentity(Namespace, Key), FLocTextIdentity(NewNamespace, NewKey)); } InOutText = FText::ChangeKey(NewNamespace, NewKey, InOutText); return true; } using FArchiveUObject::operator<<; // For visibility of the overloads we don't override virtual FArchive& operator<<(FText& Value) override { KeyText(Value); return *this; } private: TMap* PackageTextKeyMap; FString PackageNamespace; }; struct FLocArchiveInfo { FLocArchiveInfo(FString InFilename, TSharedRef InArchive) : Filename(MoveTemp(InFilename)), Archive(MoveTemp(InArchive)), bHasArchiveChanged(false) { } FString Filename; TSharedRef Archive; bool bHasArchiveChanged; }; int32 UStabilizeLocalizationKeysCommandlet::Main(const FString& Params) { // Parse command line TArray Tokens; TArray Switches; TMap Parameters; UCommandlet::ParseCommandLine(*Params, Tokens, Switches, Parameters); TSharedPtr SourceControlInfo; const bool bEnableSourceControl = Switches.Contains(TEXT("EnableSCC")); if (bEnableSourceControl) { SourceControlInfo = MakeShareable(new FLocalizationSCC()); FText SCCErrorStr; if (!SourceControlInfo->IsReady(SCCErrorStr)) { UE_LOG(LogStabilizeLocalizationKeys, Error, TEXT("Source Control error: %s"), *SCCErrorStr.ToString()); return -1; } } const bool bIncludeEngineContent = Switches.Contains(TEXT("IncludeEngine")); const bool bIncludeGameContent = Switches.Contains(TEXT("IncludeGame")); const bool bIncludePluginContent = Switches.Contains(TEXT("IncludePlugins")); const FString NativeCulture = Parameters.FindRef(TEXT("NativeCulture")); TArray RootContentPaths; FPackageName::QueryRootContentPaths(RootContentPaths); TArray AllPackages; for (const FString& RootContentPath: RootContentPaths) { // Passes path filter? const bool bIsEnginePath = RootContentPath.Equals(TEXT("/Engine/")); const bool bIsGamePath = RootContentPath.Equals(TEXT("/Game/")); const bool bIsPluginPath = !bIsEnginePath && !bIsGamePath; if ((bIsEnginePath && !bIncludeEngineContent) || (bIsGamePath && !bIncludeGameContent) || (bIsPluginPath && !bIncludePluginContent)) { UE_LOG(LogStabilizeLocalizationKeys, Display, TEXT("Skipping path '%s' as it doesn't pass the filter."), *RootContentPath); continue; } FString RootContentFilePath; if (!FPackageName::TryConvertLongPackageNameToFilename(RootContentPath, RootContentFilePath)) { UE_LOG(LogStabilizeLocalizationKeys, Display, TEXT("Skipping path '%s' as it failed to convert to a file path."), *RootContentPath); continue; } FPackageName::FindPackagesInDirectory(AllPackages, RootContentFilePath); } // Work out which packages need to be stabilized TArray UnstablePackages; for (const FString& PackageFilename: AllPackages) { if (TUniquePtr FileReader = TUniquePtr(IFileManager::Get().CreateFileReader(*PackageFilename))) { // Read package file summary from the file so we can test the version FPackageFileSummary PackageFileSummary; (*FileReader) << PackageFileSummary; const bool bRequiresKeyStabilization = !!(PackageFileSummary.PackageFlags & PKG_RequiresLocalizationGather); if (bRequiresKeyStabilization) { UnstablePackages.Add(PackageFilename); } } } // Re-key the unstable packages (in batches so we can GC at reasonable points) TMultiMap TextKeyMap; { static const int32 PackagesPerBatch = 100; const int32 NumPackages = UnstablePackages.Num(); for (int32 PackageIndex = 0; PackageIndex < NumPackages;) { const int32 PackageLimitForBatch = FMath::Min(PackageIndex + PackagesPerBatch, NumPackages); for (; PackageIndex < PackageLimitForBatch; ++PackageIndex) { const FString& PackageFilename = UnstablePackages[PackageIndex]; UE_LOG(LogStabilizeLocalizationKeys, Display, TEXT("Loading package %d of %d: '%s'."), PackageIndex + 1, NumPackages, *PackageFilename); UPackage* Package = LoadPackage(nullptr, *PackageFilename, LOAD_NoWarn | LOAD_Quiet); if (!Package) { UE_LOG(LogStabilizeLocalizationKeys, Error, TEXT("Failed to load package from: '%s'."), *PackageFilename); } else if (Package->RequiresLocalizationGather()) { // Re-key the texts in the package TMap PackageTextKeyMap; FTextKeyingArchive(Package, PackageTextKeyMap); if (PackageTextKeyMap.Num() > 0) { UE_LOG(LogStabilizeLocalizationKeys, Display, TEXT("\t%d texts stabilized in: '%s'."), PackageTextKeyMap.Num(), *PackageFilename); for (const auto& IdPair: PackageTextKeyMap) { TextKeyMap.Add(IdPair.Key, IdPair.Value); } FLocalizedAssetSCCUtil::SavePackageWithSCC(SourceControlInfo, Package, PackageFilename); } } } CollectGarbage(RF_NoFlags); } } if (TextKeyMap.Num() > 0 && !NativeCulture.IsEmpty()) { // Load up the manifests and archives - the native archives are used to find the correct source text for the foreign archives TArray> Manifests; TArray NativeLocArchives; TArray ForeignLocArchives; { TArray LocalizationPaths; if (bIncludeEngineContent) { LocalizationPaths += FPaths::GetEngineLocalizationPaths(); LocalizationPaths += FPaths::GetEditorLocalizationPaths(); } if (bIncludeGameContent) { LocalizationPaths += FPaths::GetGameLocalizationPaths(); } TArray ManifestFilenames; for (const FString& LocalizationPath: LocalizationPaths) { IFileManager::Get().FindFilesRecursive(ManifestFilenames, *LocalizationPath, TEXT("*.manifest"), /*Files*/ true, /*Directories*/ false, /*bClearFileNames*/ false); } for (const FString& ManifestFilename: ManifestFilenames) { TSharedRef InternationalizationManifest = MakeShareable(new FInternationalizationManifest()); if (FJsonInternationalizationManifestSerializer::DeserializeManifestFromFile(ManifestFilename, InternationalizationManifest)) { Manifests.Add(InternationalizationManifest); } } TArray ArchiveFilenames; for (const FString& LocalizationPath: LocalizationPaths) { IFileManager::Get().FindFilesRecursive(ArchiveFilenames, *LocalizationPath, TEXT("*.archive"), /*Files*/ true, /*Directories*/ false, /*bClearFileNames*/ false); } for (const FString& ArchiveFilename: ArchiveFilenames) { TSharedRef InternationalizationArchive = MakeShareable(new FInternationalizationArchive()); if (FJsonInternationalizationArchiveSerializer::DeserializeArchiveFromFile(ArchiveFilename, InternationalizationArchive, nullptr, nullptr)) { const FString ArchivePath = FPaths::GetPath(ArchiveFilename); const bool bIsNativeArchive = ArchivePath.EndsWith(NativeCulture); if (bIsNativeArchive) { NativeLocArchives.Add(FLocArchiveInfo(ArchiveFilename, InternationalizationArchive)); } else { ForeignLocArchives.Add(FLocArchiveInfo(ArchiveFilename, InternationalizationArchive)); } } } } // Update archives to preserve the translations from the old keys for (const auto& IdPair: TextKeyMap) { for (FLocArchiveInfo& LocArchive: ForeignLocArchives) { TSharedPtr FoundArchiveEntry = LocArchive.Archive->FindEntryByKey(IdPair.Key.GetNamespace(), IdPair.Key.GetKey(), nullptr); if (FoundArchiveEntry.IsValid() && !FoundArchiveEntry->Translation.Text.IsEmpty()) { LocArchive.bHasArchiveChanged = true; if (!LocArchive.Archive->SetTranslation(IdPair.Value.GetNamespace(), IdPair.Value.GetKey(), FoundArchiveEntry->Source, FoundArchiveEntry->Translation, FoundArchiveEntry->KeyMetadataObj)) { LocArchive.Archive->AddEntry(IdPair.Value.GetNamespace(), IdPair.Value.GetKey(), FoundArchiveEntry->Source, FoundArchiveEntry->Translation, FoundArchiveEntry->KeyMetadataObj, FoundArchiveEntry->bIsOptional); } } } } // Re-save any updated archives for (const FLocArchiveInfo& LocArchive: ForeignLocArchives) { if (LocArchive.bHasArchiveChanged) { const bool bDidWriteArchive = FLocalizedAssetSCCUtil::SaveFileWithSCC(SourceControlInfo, LocArchive.Filename, [&LocArchive](const FString& InSaveFileName) -> bool { return FJsonInternationalizationArchiveSerializer::SerializeArchiveToFile(LocArchive.Archive, InSaveFileName); }); if (!bDidWriteArchive) { UE_LOG(LogStabilizeLocalizationKeys, Error, TEXT("Failed to write archive to %s."), *LocArchive.Filename); return false; } } } } return 0; } #else // USE_STABLE_LOCALIZATION_KEYS int32 UStabilizeLocalizationKeysCommandlet::Main(const FString& Params) { UE_LOG(LogStabilizeLocalizationKeys, Fatal, TEXT("UStabilizeLocalizationKeysCommandlet requires a build with USE_STABLE_LOCALIZATION_KEYS enabled!")); return 0; } #endif // USE_STABLE_LOCALIZATION_KEYS