Files
UnrealEngine/Engine/Source/Developer/Localization/Private/LocTextHelper.cpp
2025-05-18 13:04:45 +08:00

1972 lines
71 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "LocTextHelper.h"
#include "Dom/JsonObject.h"
#include "Internationalization/BreakIterator.h"
#include "Internationalization/IBreakIterator.h"
#include "Logging/StructuredLog.h"
#include "Misc/DataDrivenPlatformInfoRegistry.h"
#include "Misc/EnumClassFlags.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"
#include "PlatformInfo.h"
#include "Serialization/Csv/CsvParser.h"
#include "Serialization/JsonInternationalizationArchiveSerializer.h"
#include "Serialization/JsonInternationalizationManifestSerializer.h"
#include "Serialization/JsonInternationalizationMetadataSerializer.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(LocTextHelper)
#define LOCTEXT_NAMESPACE "LocTextHelper"
DEFINE_LOG_CATEGORY_STATIC(LogLocTextHelper, Log, All);
namespace LocTextHelper
{
static constexpr int32 LocalizationLogIdentifier = 304;
}
bool FLocTextPlatformSplitUtils::ShouldSplitPlatformData(const ELocTextPlatformSplitMode& InSplitMode)
{
return InSplitMode != ELocTextPlatformSplitMode::None;
}
const TArray<FString>& FLocTextPlatformSplitUtils::GetPlatformsToSplit(const ELocTextPlatformSplitMode& InSplitMode)
{
switch (InSplitMode)
{
case ELocTextPlatformSplitMode::Confidential:
{
static const TArray<FString> ConfidentialPlatformNames = []()
{
TArray<FString> TmpArray;
for (FName Name : FDataDrivenPlatformInfoRegistry::GetConfidentialPlatforms())
{
TmpArray.Add(Name.ToString());
}
TmpArray.Sort();
return TmpArray;
}();
return ConfidentialPlatformNames;
}
case ELocTextPlatformSplitMode::All:
{
static const TArray<FString> AllPlatformNames = []()
{
// the Keys of the FDataDrivenPlatformInfoRegistry platforms are the known ini platform names
TArray<FString> TmpArray;
for (FName Name : FDataDrivenPlatformInfoRegistry::GetSortedPlatformNames(EPlatformInfoType::AllPlatformInfos))
{
TmpArray.Add(Name.ToString());
}
return TmpArray;
}();
return AllPlatformNames;
}
default:
break;
}
static TArray<FString> EmptyArray;
return EmptyArray;
}
void FLocTextConflicts::AddConflict(const FLocKey& InNamespace, const FLocKey& InKey, const TSharedPtr<FLocMetadataObject>& InKeyMetadata, const FLocItem& InSource, const FString& InSourceLocation)
{
TSharedPtr<FConflict> ExistingEntry = FindEntryByKey(InNamespace, InKey, InKeyMetadata);
if (!ExistingEntry.IsValid())
{
TSharedRef<FConflict> NewEntry = MakeShared<FConflict>(InNamespace, InKey, InKeyMetadata);
EntriesByKey.Add(InKey, NewEntry);
ExistingEntry = NewEntry;
}
ExistingEntry->Add(InSource, InSourceLocation.ReplaceCharWithEscapedChar());
}
TSharedPtr<FLocTextConflicts::FConflict> FLocTextConflicts::FindEntryByKey(const FLocKey& InNamespace, const FLocKey& InKey, const TSharedPtr<FLocMetadataObject> InKeyMetadata) const
{
TArray<TSharedRef<FConflict>> MatchingEntries;
EntriesByKey.MultiFind(InKey, MatchingEntries);
for (const TSharedRef<FConflict>& Entry : MatchingEntries)
{
if (Entry->Namespace == InNamespace)
{
if (InKeyMetadata.IsValid() != Entry->KeyMetadataObj.IsValid())
{
continue;
}
else if ((!InKeyMetadata.IsValid() && !Entry->KeyMetadataObj.IsValid()) || (*InKeyMetadata == *Entry->KeyMetadataObj))
{
return Entry;
}
}
}
return nullptr;
}
FString FLocTextConflicts::GetConflictReport() const
{
FString Report;
for (const auto& ConflictPair : EntriesByKey)
{
const TSharedRef<FConflict>& Conflict = ConflictPair.Value;
const FString& Namespace = Conflict->Namespace.GetString();
const FString& Key = Conflict->Key.GetString();
bool bAddToReport = false;
// Grab the list of all conflicting source text associated with this key
TArray<FLocItem> SourceList;
Conflict->EntriesBySourceLocation.GenerateValueArray(SourceList);
if (SourceList.Num() >= 2)
{
for (int32 i = 0; i < SourceList.Num() - 1 && !bAddToReport; ++i)
{
// we only add the conflict to the report if the source strings do not match up.
for (int32 j = i + 1; j < SourceList.Num() && !bAddToReport; ++j)
{
if (!(SourceList[i] == SourceList[j]))
{
bAddToReport = true;
}
}
}
}
if (bAddToReport)
{
FString KeyMetadataString = FJsonInternationalizationMetaDataSerializer::MetadataToString(Conflict->KeyMetadataObj);
Report += FString::Printf(TEXT("%s - %s %s\n"), *Namespace, *Key, *KeyMetadataString);
for (auto EntryIter = Conflict->EntriesBySourceLocation.CreateConstIterator(); EntryIter; ++EntryIter)
{
const FString& SourceLocation = EntryIter.Key();
FString ProcessedSourceLocation = FPaths::ConvertRelativePathToFull(SourceLocation);
ProcessedSourceLocation.ReplaceInline(TEXT("\\"), TEXT("/"));
ProcessedSourceLocation.ReplaceInline(*FPaths::RootDir(), TEXT("/"));
const FString& SourceText = EntryIter.Value().Text.ReplaceCharWithEscapedChar();
FString SourceMetadataString = FJsonInternationalizationMetaDataSerializer::MetadataToString(EntryIter.Value().MetadataObj);
Report += FString::Printf(TEXT("\t%s - \"%s\" %s\n"), *ProcessedSourceLocation, *SourceText, *SourceMetadataString);
}
Report += TEXT("\n");
}
}
return Report;
}
FString FLocTextConflicts::GetConflictReportAsCSV() const
{
// First have the headers
FString Report = TEXT("Namespace,Key,File1,Line1,File2,Line2,Source1,Source2\n");
for (const auto& ConflictPair : EntriesByKey)
{
const TSharedRef<FConflict>& Conflict = ConflictPair.Value;
const FString& Namespace = Conflict->Namespace.GetString();
const FString& Key = Conflict->Key.GetString();
bool bAddToReport = false;
TArray<FLocItem> SourceList;
Conflict->EntriesBySourceLocation.GenerateValueArray(SourceList);
if (SourceList.Num() >= 2)
{
for (int32 i = 0; i < SourceList.Num() - 1 && !bAddToReport; ++i)
{
for (int32 j = i + 1; j < SourceList.Num() && !bAddToReport; ++j)
{
// Conflict only if the source text and metadata don't match
if (!(SourceList[i] == SourceList[j]))
{
bAddToReport = true;
}
}
}
}
if (bAddToReport)
{
// the format of the CSV will need file1,line1 and file2,line2 to be compared against each other
// If a single key has more than 1 conflict, we want file 1 in the csv to always be the same one.
// Thus we store the results of the first entry to be appended in each row of the CSV for a single entry
bool bFirstEntry = true;
FString FirstEntrySourceFile;
FString FirstEntryLineNumber;
FString FirstEntrySourceText;
// This maps each of the source locations to its conflicting source string
// for each entry in this, we create a new row in the .csv
for (auto EntryIter = Conflict->EntriesBySourceLocation.CreateConstIterator(); EntryIter; ++EntryIter)
{
const FString& SourceLocation = EntryIter.Key();
FString ProcessedSourceLocation = SourceLocation;
ProcessedSourceLocation.ReplaceInline(TEXT("\\"), TEXT("/"));
const FString& SourceText = EntryIter.Value().Text.ReplaceCharWithEscapedChar();
// in the event there are more than 2 key conflicts for a single key,
// this will always be the entry for file 1 and line 1 in the output file
if (bFirstEntry)
{
// For a source file conflict, source location is in the format My/File/Name.cpp:LineNumber
bool bSplitSuccessful = ProcessedSourceLocation.Split(TEXT(":"), &FirstEntrySourceFile, &FirstEntryLineNumber, ESearchCase::IgnoreCase, ESearchDir::FromEnd);
// If the split fails, this means that the location is not in the form file:linenumber.
// if the line number string is not numeric, this means we are dealing with an asset path
if (!bSplitSuccessful || !FirstEntryLineNumber.IsNumeric())
{
FirstEntrySourceFile = ProcessedSourceLocation;
FirstEntryLineNumber = TEXT("0");
}
FirstEntrySourceText = SourceText;
bFirstEntry = false;
continue;
}
FString CurrentSourceFile;
FString CurrentLineNumber;
bool bSplitSuccessful = ProcessedSourceLocation.Split(TEXT(":"), &CurrentSourceFile, &CurrentLineNumber, ESearchCase::IgnoreCase, ESearchDir::FromEnd);
// Same as above
if (!bSplitSuccessful || !CurrentLineNumber.IsNumeric())
{
CurrentSourceFile = ProcessedSourceLocation;
CurrentLineNumber = TEXT("0");
}
Report += FString::Printf(TEXT("%s,%s,%s,%s,%s,%s,%s,%s\n"), *Namespace, *Key, *FirstEntrySourceFile, *FirstEntryLineNumber, *CurrentSourceFile, *CurrentLineNumber, *FirstEntrySourceText, *SourceText);
}
}
}
return Report;
}
const FString FLocTextWordCounts::ColHeadingDateTime = TEXT("Date/Time");
const FString FLocTextWordCounts::ColHeadingWordCount = TEXT("Word Count");
void FLocTextWordCounts::FRowData::ResetWordCounts()
{
SourceWordCount = 0;
PerCultureWordCounts.Reset();
}
bool FLocTextWordCounts::FRowData::IdenticalWordCounts(const FRowData& InOther) const
{
if (SourceWordCount != InOther.SourceWordCount)
{
return false;
}
if (PerCultureWordCounts.Num() != InOther.PerCultureWordCounts.Num())
{
return false;
}
for (const auto& PerCultureWordCountPair : PerCultureWordCounts)
{
int32 OtherPerCultureWordCount = 0;
if (const int32* FoundOtherPerCultureWordCount = InOther.PerCultureWordCounts.Find(PerCultureWordCountPair.Key))
{
OtherPerCultureWordCount = *FoundOtherPerCultureWordCount;
}
if (OtherPerCultureWordCount != PerCultureWordCountPair.Value)
{
return false;
}
}
return true;
}
FLocTextWordCounts::FRowData& FLocTextWordCounts::AddRow(int32* OutIndex)
{
const int32 RowIndex = Rows.AddDefaulted();
if (OutIndex)
{
*OutIndex = RowIndex;
}
return Rows[RowIndex];
}
FLocTextWordCounts::FRowData* FLocTextWordCounts::GetRow(const int32 InIndex)
{
return Rows.IsValidIndex(InIndex) ? &Rows[InIndex] : nullptr;
}
const FLocTextWordCounts::FRowData* FLocTextWordCounts::GetRow(const int32 InIndex) const
{
return Rows.IsValidIndex(InIndex) ? &Rows[InIndex] : nullptr;
}
int32 FLocTextWordCounts::GetRowCount() const
{
return Rows.Num();
}
void FLocTextWordCounts::TrimReport()
{
SortRowsByDate();
for (int32 RowIndex = 1; RowIndex < Rows.Num(); ++RowIndex)
{
const FRowData& PreviousRowData = Rows[RowIndex - 1];
const FRowData& CurrentRowData = Rows[RowIndex];
if (PreviousRowData.IdenticalWordCounts(CurrentRowData))
{
Rows.RemoveAt(RowIndex--, EAllowShrinking::No);
continue;
}
}
}
bool FLocTextWordCounts::FromCSV(const FString& InCSVString, FText* OutError)
{
const FCsvParser CsvParser(InCSVString);
const auto& CsvRows = CsvParser.GetRows();
// Must have at least 2 rows (timestamp + source word count)
if (CsvRows.Num() <= 1)
{
if (OutError)
{
*OutError = FText::Format(LOCTEXT("Error_WordCountsFromCSV_TooFewRows", "Failed to parse the CSV string as it contained too few rows (expected at least 2, got {0})."), CsvRows.Num());
}
return false;
}
int32 DateTimeColumn = INDEX_NONE;
int32 WordCountColumn = INDEX_NONE;
TMap<FString, int32> PerCultureColumns;
// Make sure our header has the required columns
{
const TArray<const TCHAR*>& CsvCells = CsvRows[0];
for (int32 CellIdx = 0; CellIdx < CsvCells.Num(); ++CellIdx)
{
const TCHAR* Cell = CsvCells[CellIdx];
if (FCString::Stricmp(Cell, *ColHeadingDateTime) == 0 && DateTimeColumn == INDEX_NONE)
{
DateTimeColumn = CellIdx;
}
else if (FCString::Stricmp(Cell, *ColHeadingWordCount) == 0 && WordCountColumn == INDEX_NONE)
{
WordCountColumn = CellIdx;
}
else
{
PerCultureColumns.Add(Cell, CellIdx);
}
}
const bool bValidHeader = DateTimeColumn != INDEX_NONE && WordCountColumn != INDEX_NONE;
if (!bValidHeader)
{
if (OutError)
{
*OutError = FText::Format(LOCTEXT("Error_WordCountsFromCSV_InvalidHeader", "Failed to parse the CSV string as the header was missing one of the required rows (either '{0}' or '{1}')."), FText::FromString(ColHeadingDateTime), FText::FromString(ColHeadingWordCount));
}
return false;
}
}
// Perform the import
Rows.Reset(CsvRows.Num() - 1);
for (int32 CsvRowIdx = 1; CsvRowIdx < CsvRows.Num(); ++CsvRowIdx)
{
const TArray<const TCHAR*>& CsvCells = CsvRows[CsvRowIdx];
// Must have at least an entry for the required columns
if (CsvCells.IsValidIndex(DateTimeColumn) && CsvCells.IsValidIndex(WordCountColumn))
{
FRowData& RowData = Rows[Rows.AddDefaulted()];
// Parse required data
FDateTime::Parse(CsvCells[DateTimeColumn], RowData.Timestamp);
LexFromString(RowData.SourceWordCount, CsvCells[WordCountColumn]);
// Parse per-culture data
for (const auto& PerCultureColumnPair : PerCultureColumns)
{
if (CsvCells.IsValidIndex(PerCultureColumnPair.Value))
{
int32 PerCultureWordCount = 0;
LexFromString(PerCultureWordCount, CsvCells[PerCultureColumnPair.Value]);
RowData.PerCultureWordCounts.Add(PerCultureColumnPair.Key, PerCultureWordCount);
}
}
}
}
return true;
}
FString FLocTextWordCounts::ToCSV()
{
SortRowsByDate();
// Collect and sort the per-culture column names used by any row
TArray<FString> PerCultureColumnNames;
for (const FRowData& RowData : Rows)
{
for (const auto& PerCultureWordCountPair : RowData.PerCultureWordCounts)
{
PerCultureColumnNames.AddUnique(PerCultureWordCountPair.Key);
}
}
PerCultureColumnNames.Sort();
FString CSVString;
// Write the header
{
CSVString += ColHeadingDateTime;
CSVString += TEXT(",");
CSVString += ColHeadingWordCount;
for (const FString& PerCultureColumnName : PerCultureColumnNames)
{
CSVString += TEXT(",");
CSVString += PerCultureColumnName;
}
CSVString += TEXT("\n");
}
// Write each row
for (const FRowData& RowData : Rows)
{
CSVString += RowData.Timestamp.ToString();
CSVString += TEXT(",");
CSVString += FString::Printf(TEXT("%d"), RowData.SourceWordCount);
for (const FString& PerCultureColumnName : PerCultureColumnNames)
{
int32 PerCultureWordCount = 0;
if (const int32* FoundPerCultureWordCount = RowData.PerCultureWordCounts.Find(PerCultureColumnName))
{
PerCultureWordCount = *FoundPerCultureWordCount;
}
CSVString += TEXT(",");
CSVString += FString::Printf(TEXT("%d"), PerCultureWordCount);
}
CSVString += TEXT("\n");
}
return CSVString;
}
void FLocTextWordCounts::SortRowsByDate()
{
Rows.Sort([](const FRowData& InOne, const FRowData& InTwo)
{
return InOne.Timestamp < InTwo.Timestamp;
});
}
FLocTextHelper::FLocTextHelper(FString InTargetName, TSharedPtr<ILocFileNotifies> InLocFileNotifies, const ELocTextPlatformSplitMode InPlatformSplitMode)
: PlatformSplitMode(InPlatformSplitMode)
, TargetName(MoveTemp(InTargetName))
, LocFileNotifies(MoveTemp(InLocFileNotifies))
{
}
FLocTextHelper::FLocTextHelper(FString InTargetPath, FString InManifestName, FString InArchiveName, FString InNativeCulture, TArray<FString> InForeignCultures, TSharedPtr<ILocFileNotifies> InLocFileNotifies, const ELocTextPlatformSplitMode InPlatformSplitMode)
: PlatformSplitMode(InPlatformSplitMode)
, TargetPath(MoveTemp(InTargetPath))
, ManifestName(MoveTemp(InManifestName))
, ArchiveName(MoveTemp(InArchiveName))
, NativeCulture(MoveTemp(InNativeCulture))
, ForeignCultures(MoveTemp(InForeignCultures))
, LocFileNotifies(MoveTemp(InLocFileNotifies))
{
checkf(!TargetPath.IsEmpty(), TEXT("Target path may not be empty!"));
checkf(!ManifestName.IsEmpty(), TEXT("Manifest name may not be empty!"));
checkf(!ArchiveName.IsEmpty(), TEXT("Archive name may not be empty!"));
// todo: We currently infer the target name from the manifest, however once all target files are named consistently the target name should be passed in rather than the manifest/archive names
TargetName = FPaths::GetBaseFilename(ManifestName);
// Make sure the native culture isn't in the list of foreign cultures
if (!NativeCulture.IsEmpty())
{
ForeignCultures.Remove(NativeCulture);
}
}
bool FLocTextHelper::ShouldSplitPlatformData() const
{
return FLocTextPlatformSplitUtils::ShouldSplitPlatformData(PlatformSplitMode);
}
ELocTextPlatformSplitMode FLocTextHelper::GetPlatformSplitMode() const
{
return PlatformSplitMode;
}
const TArray<FString>& FLocTextHelper::GetPlatformsToSplit() const
{
return FLocTextPlatformSplitUtils::GetPlatformsToSplit(PlatformSplitMode);
}
const FString& FLocTextHelper::GetTargetName() const
{
return TargetName;
}
const FString& FLocTextHelper::GetTargetPath() const
{
return TargetPath;
}
TSharedPtr<ILocFileNotifies> FLocTextHelper::GetLocFileNotifies() const
{
return LocFileNotifies;
}
void FLocTextHelper::SetCopyrightNotice(const FString& InCopyrightNotice)
{
CopyrightNotice = InCopyrightNotice;
}
const FString& FLocTextHelper::GetCopyrightNotice() const
{
return CopyrightNotice;
}
const FString& FLocTextHelper::GetNativeCulture() const
{
return NativeCulture;
}
const TArray<FString>& FLocTextHelper::GetForeignCultures() const
{
return ForeignCultures;
}
TArray<FString> FLocTextHelper::GetAllCultures(const bool bSingleCultureMode) const
{
// Single-culture mode is a hack for the Localization commandlets
// In this mode we only include the native culture if we have no foreign cultures
const bool bIncludeNativeCulture = (!bSingleCultureMode || ForeignCultures.Num() == 0) && !NativeCulture.IsEmpty();
TArray<FString> AllCultures;
if (bIncludeNativeCulture)
{
AllCultures.Add(NativeCulture);
}
AllCultures.Append(ForeignCultures);
return AllCultures;
}
bool FLocTextHelper::HasManifest() const
{
return Manifest.IsValid();
}
bool FLocTextHelper::LoadManifest(const ELocTextHelperLoadFlags InLoadFlags, FText* OutError)
{
const FString ManifestFilePath = TargetPath / ManifestName;
return LoadManifest(ManifestFilePath, InLoadFlags, OutError);
}
bool FLocTextHelper::LoadManifest(const FString& InManifestFilePath, const ELocTextHelperLoadFlags InLoadFlags, FText* OutError)
{
Manifest.Reset();
Manifest = LoadManifestImpl(InManifestFilePath, InLoadFlags, OutError);
return Manifest.IsValid();
}
bool FLocTextHelper::SaveManifest(FText* OutError) const
{
const FString ManifestFilePath = TargetPath / ManifestName;
return SaveManifest(ManifestFilePath, OutError);
}
bool FLocTextHelper::SaveManifest(const FString& InManifestFilePath, FText* OutError) const
{
if (!Manifest.IsValid())
{
if (OutError)
{
*OutError = FText::Format(LOCTEXT("Error_SaveManifest_NoManifest", "Failed to save file '{0}' as there is no manifest instance to save."), FText::FromString(InManifestFilePath));
}
return false;
}
return SaveManifestImpl(Manifest.ToSharedRef(), InManifestFilePath, OutError);
}
bool FLocTextHelper::SerializeManifestToJson(TSharedRef<FJsonObject> JsonObject)
{
return FJsonInternationalizationManifestSerializer::SerializeManifest(Manifest.ToSharedRef(), JsonObject);
}
void FLocTextHelper::TrimManifest()
{
if (Dependencies.Num() > 0)
{
// We'll generate a new manifest by only including items that are not in the dependencies
TSharedRef<FInternationalizationManifest> TrimmedManifest = MakeShared<FInternationalizationManifest>();
for (FManifestEntryByStringContainer::TConstIterator It(Manifest->GetEntriesBySourceTextIterator()); It; ++It)
{
const TSharedRef<FManifestEntry> ManifestEntry = It.Value();
for (const FManifestContext& Context : ManifestEntry->Contexts)
{
FString DependencyFileName;
TSharedPtr<FManifestEntry> DependencyEntry = FindDependencyEntry(ManifestEntry->Namespace, Context, &DependencyFileName);
// Ignore this dependency if the platforms are different
if (DependencyEntry.IsValid())
{
const FManifestContext* DependencyContext = DependencyEntry->FindContext(Context.Key, Context.KeyMetadataObj);
if (Context.PlatformName != DependencyContext->PlatformName)
{
DependencyEntry.Reset();
DependencyFileName.Reset();
}
}
if (DependencyEntry.IsValid())
{
if (!(DependencyEntry->Source.IsExactMatch(ManifestEntry->Source)))
{
const FManifestContext* ConflictingContext = DependencyEntry->FindContext(Context.Key, Context.KeyMetadataObj);
const FString DependencyEntryFullSrcLoc = (!DependencyFileName.IsEmpty()) ? DependencyFileName : ConflictingContext->SourceLocation;
// There is a dependency manifest entry that has the same namespace and keys as our main manifest entry but the source text differs.
UE_LOGFMT(LogLocTextHelper, Warning, "Text conflict for namespace '{locNamespace}' and key '{locKey}'{locKeyMetaData}. Main manifest entry is '{text}'{textMetaData}. Conflict from manifest dependency is {conflictLocation}:'{conflictText}'{conflictTextMetaData}.",
("locNamespace", *SanitizeLogOutput(ManifestEntry->Namespace.GetString())),
("locKey", *SanitizeLogOutput(Context.Key.GetString())),
("locKeyMetaData", (Context.KeyMetadataObj ? *FString::Printf(TEXT(" (meta-data \"%s\")"), *SanitizeLogOutput(FJsonInternationalizationMetaDataSerializer::MetadataToString(Context.KeyMetadataObj))) : TEXT(""))),
("text", *SanitizeLogOutput(ManifestEntry->Source.Text)),
("textMetaData", (ManifestEntry->Source.MetadataObj ? *FString::Printf(TEXT(" (meta-data \"%s\")"), *SanitizeLogOutput(FJsonInternationalizationMetaDataSerializer::MetadataToString(ManifestEntry->Source.MetadataObj))) : TEXT(""))),
("conflictLocation", *DependencyFileName),
("conflictText", *SanitizeLogOutput(DependencyEntry->Source.Text)),
("conflictTextMetaData", (DependencyEntry->Source.MetadataObj ? *FString::Printf(TEXT(" (meta-data \"%s\")"), *SanitizeLogOutput(FJsonInternationalizationMetaDataSerializer::MetadataToString(DependencyEntry->Source.MetadataObj))) : TEXT(""))),
("id", LocTextHelper::LocalizationLogIdentifier)
);
ConflictTracker.AddConflict(ManifestEntry->Namespace, Context.Key, Context.KeyMetadataObj, ManifestEntry->Source, *Context.SourceLocation);
ConflictTracker.AddConflict(ManifestEntry->Namespace, Context.Key, Context.KeyMetadataObj, DependencyEntry->Source, DependencyEntryFullSrcLoc);
}
}
else
{
// Since we did not find any entries in the dependencies list that match, we'll add to the new manifest
const bool bAddSuccessful = TrimmedManifest->AddSource(ManifestEntry->Namespace, ManifestEntry->Source, Context);
if (!bAddSuccessful)
{
UE_LOGFMT(LogLocTextHelper, Error, "Failed to add text for namespace '{locNamespace}' and key '{locKey}'{locKeyMetaData} with source '{text}'{textMetaData} from manifest dependency '{file}'.",
("file", *DependencyFileName),
("locNamespace", *ManifestEntry->Namespace.GetString()),
("locKey", *Context.Key.GetString()),
("locKeyMetaData", (Context.KeyMetadataObj ? *FString::Printf(TEXT(" (meta-data \"%s\")"), *FJsonInternationalizationMetaDataSerializer::MetadataToString(Context.KeyMetadataObj)) : TEXT(""))),
("text", *ManifestEntry->Source.Text),
("textMetaData", (ManifestEntry->Source.MetadataObj ? *FString::Printf(TEXT(" (meta-data \"%s\")"), *FJsonInternationalizationMetaDataSerializer::MetadataToString(ManifestEntry->Source.MetadataObj)) : TEXT(""))),
("id", LocTextHelper::LocalizationLogIdentifier)
);
}
}
}
}
Manifest = TrimmedManifest;
}
}
bool FLocTextHelper::HasNativeArchive() const
{
return HasArchive(NativeCulture);
}
bool FLocTextHelper::LoadNativeArchive(const ELocTextHelperLoadFlags InLoadFlags, FText* OutError)
{
return LoadArchive(NativeCulture, InLoadFlags, OutError);
}
bool FLocTextHelper::LoadNativeArchive(const FString& InArchiveFilePath, const ELocTextHelperLoadFlags InLoadFlags, FText* OutError)
{
return LoadArchive(NativeCulture, InArchiveFilePath, InLoadFlags, OutError);
}
bool FLocTextHelper::SaveNativeArchive(FText* OutError) const
{
return SaveArchive(NativeCulture, OutError);
}
bool FLocTextHelper::SaveNativeArchive(const FString& InArchiveFilePath, FText* OutError) const
{
return SaveArchive(NativeCulture, InArchiveFilePath, OutError);
}
bool FLocTextHelper::HasForeignArchive(const FString& InCulture) const
{
checkf(ForeignCultures.Contains(InCulture), TEXT("Attempted to check for a foreign culture archive file, but the given culture (%s) wasn't set during construction!"), *InCulture);
return HasArchive(InCulture);
}
bool FLocTextHelper::LoadForeignArchive(const FString& InCulture, const ELocTextHelperLoadFlags InLoadFlags, FText* OutError)
{
checkf(ForeignCultures.Contains(InCulture), TEXT("Attempted to load a foreign culture archive file, but the given culture (%s) wasn't set during construction!"), *InCulture);
return LoadArchive(InCulture, InLoadFlags, OutError);
}
bool FLocTextHelper::LoadForeignArchive(const FString& InCulture, const FString& InArchiveFilePath, const ELocTextHelperLoadFlags InLoadFlags, FText* OutError)
{
checkf(ForeignCultures.Contains(InCulture), TEXT("Attempted to load a foreign culture archive file, but the given culture (%s) wasn't set during construction!"), *InCulture);
return LoadArchive(InCulture, InArchiveFilePath, InLoadFlags, OutError);
}
bool FLocTextHelper::SaveForeignArchive(const FString& InCulture, FText* OutError) const
{
checkf(ForeignCultures.Contains(InCulture), TEXT("Attempted to load a foreign culture archive file, but the given culture (%s) wasn't set during construction!"), *InCulture);
return SaveArchive(InCulture, OutError);
}
bool FLocTextHelper::SaveForeignArchive(const FString& InCulture, const FString& InArchiveFilePath, FText* OutError) const
{
checkf(ForeignCultures.Contains(InCulture), TEXT("Attempted to load a foreign culture archive file, but the given culture (%s) wasn't set during construction!"), *InCulture);
return SaveArchive(InCulture, InArchiveFilePath, OutError);
}
bool FLocTextHelper::HasArchive(const FString& InCulture) const
{
TSharedPtr<FInternationalizationArchive> Archive = Archives.FindRef(InCulture);
return Archive.IsValid();
}
bool FLocTextHelper::LoadArchive(const FString& InCulture, const ELocTextHelperLoadFlags InLoadFlags, FText* OutError)
{
const FString ArchiveFilePath = TargetPath / InCulture / ArchiveName;
return LoadArchive(InCulture, ArchiveFilePath, InLoadFlags, OutError);
}
bool FLocTextHelper::LoadArchive(const FString& InCulture, const FString& InArchiveFilePath, const ELocTextHelperLoadFlags InLoadFlags, FText* OutError)
{
const bool bIsNativeArchive = !NativeCulture.IsEmpty() && InCulture == NativeCulture;
const bool bIsForeignArchive = ForeignCultures.Contains(InCulture);
checkf(bIsNativeArchive || bIsForeignArchive, TEXT("Attempted to load a culture archive file, but the given culture (%s) wasn't set during construction!"), *InCulture);
checkf(Manifest.IsValid(), TEXT("Attempted to load a culture archive file, but no manifest has been loaded!"));
Archives.Remove(InCulture);
TSharedPtr<FInternationalizationArchive> Archive = LoadArchiveImpl(InArchiveFilePath, InLoadFlags, OutError);
if (Archive.IsValid())
{
Archives.Add(InCulture, Archive);
return true;
}
return false;
}
bool FLocTextHelper::SaveArchive(const FString& InCulture, FText* OutError) const
{
const FString ArchiveFilePath = TargetPath / InCulture / ArchiveName;
return SaveArchive(InCulture, ArchiveFilePath, OutError);
}
bool FLocTextHelper::SaveArchive(const FString& InCulture, const FString& InArchiveFilePath, FText* OutError) const
{
const bool bIsNativeArchive = !NativeCulture.IsEmpty() && InCulture == NativeCulture;
const bool bIsForeignArchive = ForeignCultures.Contains(InCulture);
checkf(bIsNativeArchive || bIsForeignArchive, TEXT("Attempted to save a culture archive file, but the given culture (%s) wasn't set during construction!"), *InCulture);
TSharedPtr<FInternationalizationArchive> Archive = Archives.FindRef(InCulture);
if (!Archive.IsValid())
{
if (OutError)
{
*OutError = FText::Format(LOCTEXT("Error_SaveArchive_NoArchive", "Failed to save file '{0}' as there is no archive instance to save."), FText::FromString(InArchiveFilePath));
}
return false;
}
return SaveArchiveImpl(Archive.ToSharedRef(), InArchiveFilePath, OutError);
}
bool FLocTextHelper::LoadAllArchives(const ELocTextHelperLoadFlags InLoadFlags, FText* OutError)
{
if (!NativeCulture.IsEmpty() && !LoadNativeArchive(InLoadFlags, OutError))
{
return false;
}
for (const FString& Culture : ForeignCultures)
{
if (!LoadForeignArchive(Culture, InLoadFlags, OutError))
{
return false;
}
}
return true;
}
bool FLocTextHelper::SaveAllArchives(FText* OutError) const
{
if (!NativeCulture.IsEmpty() && !SaveNativeArchive(OutError))
{
return false;
}
for (const FString& Culture : ForeignCultures)
{
if (!SaveForeignArchive(Culture, OutError))
{
return false;
}
}
return true;
}
void FLocTextHelper::TrimArchive(const FString& InCulture)
{
checkf(Manifest.IsValid(), TEXT("Attempted to trim an archive file, but no manifest has been loaded!"));
TSharedPtr<FInternationalizationArchive> Archive = Archives.FindRef(InCulture);
checkf(Archive.IsValid(), TEXT("Attempted to trim an archive file, but no valid archive could be found for '%s'!"), *InCulture);
TSharedPtr<FInternationalizationArchive> NativeArchive;
if (!NativeCulture.IsEmpty() && InCulture != NativeCulture)
{
NativeArchive = Archives.FindRef(NativeCulture);
checkf(NativeArchive.IsValid(), TEXT("Attempted to trim an archive file, but no valid archive could be found for '%s'!"), *NativeCulture);
}
// Copy any translations that match current manifest entries over into the trimmed archive
TSharedRef<FInternationalizationArchive> TrimmedArchive = MakeShared<FInternationalizationArchive>();
EnumerateSourceTexts([&](TSharedRef<FManifestEntry> InManifestEntry) -> bool
{
for (const FManifestContext& Context : InManifestEntry->Contexts)
{
// Keep any translation for the source text
TSharedPtr<FArchiveEntry> ArchiveEntry = Archive->FindEntryByKey(InManifestEntry->Namespace, Context.Key, Context.KeyMetadataObj);
if (ArchiveEntry.IsValid())
{
TrimmedArchive->AddEntry(ArchiveEntry.ToSharedRef());
}
}
return true; // continue enumeration
}, true);
Archives.Remove(InCulture);
Archives.Add(InCulture, TrimmedArchive);
}
bool FLocTextHelper::LoadAll(const ELocTextHelperLoadFlags InLoadFlags, FText* OutError)
{
if (!LoadManifest(InLoadFlags, OutError))
{
return false;
}
return LoadAllArchives(InLoadFlags, OutError);
}
bool FLocTextHelper::SaveAll(FText* OutError) const
{
if (!SaveManifest(OutError))
{
return false;
}
return SaveAllArchives(OutError);
}
bool FLocTextHelper::AddDependency(const FString& InDependencyFilePath, FText* OutError)
{
if (DependencyPaths.Contains(InDependencyFilePath))
{
return true;
}
TSharedPtr<FInternationalizationManifest> DepManifest = LoadManifestImpl(InDependencyFilePath, ELocTextHelperLoadFlags::Load, OutError);
if (DepManifest.IsValid())
{
DependencyPaths.Add(InDependencyFilePath);
Dependencies.Add(DepManifest);
return true;
}
return false;
}
TSharedPtr<FManifestEntry> FLocTextHelper::FindDependencyEntry(const FLocKey& InNamespace, const FLocKey& InKey, const FString* InSourceText, FString* OutDependencyFilePath) const
{
for (int32 DepIndex = 0; DepIndex < Dependencies.Num(); ++DepIndex)
{
TSharedPtr<FInternationalizationManifest> DepManifest = Dependencies[DepIndex];
const TSharedPtr<FManifestEntry> DepEntry = DepManifest->FindEntryByKey(InNamespace, InKey, InSourceText);
if (DepEntry.IsValid())
{
if (OutDependencyFilePath)
{
*OutDependencyFilePath = DependencyPaths[DepIndex];
}
return DepEntry;
}
}
return nullptr;
}
TSharedPtr<FManifestEntry> FLocTextHelper::FindDependencyEntry(const FLocKey& InNamespace, const FManifestContext& InContext, FString* OutDependencyFilePath) const
{
for (int32 DepIndex = 0; DepIndex < Dependencies.Num(); ++DepIndex)
{
TSharedPtr<FInternationalizationManifest> DepManifest = Dependencies[DepIndex];
const TSharedPtr<FManifestEntry> DepEntry = DepManifest->FindEntryByContext(InNamespace, InContext);
if (DepEntry.IsValid())
{
if (OutDependencyFilePath)
{
*OutDependencyFilePath = DependencyPaths[DepIndex];
}
return DepEntry;
}
}
return nullptr;
}
bool FLocTextHelper::AddSourceText(const FLocKey& InNamespace, const FLocItem& InSource, const FManifestContext& InContext, const FString* InDescription)
{
checkf(Manifest.IsValid(), TEXT("Attempted to add source text, but no manifest has been loaded!"));
bool bAddSuccessful = false;
// Check if the entry already exists in the manifest or one of the manifest dependencies
FString ExistingEntryFileName;
TSharedPtr<FManifestEntry> ExistingEntry = Manifest->FindEntryByContext(InNamespace, InContext);
if (!ExistingEntry.IsValid())
{
ExistingEntry = FindDependencyEntry(InNamespace, InContext, &ExistingEntryFileName);
// Ignore this dependency if the platforms are different
if (ExistingEntry.IsValid())
{
const FManifestContext* DependencyContext = ExistingEntry->FindContext(InContext.Key, InContext.KeyMetadataObj);
if (InContext.PlatformName != DependencyContext->PlatformName)
{
ExistingEntry.Reset();
ExistingEntryFileName.Reset();
}
}
}
if (ExistingEntry.IsValid())
{
if (InSource.IsExactMatch(ExistingEntry->Source))
{
bAddSuccessful = true;
ExistingEntry->MergeContextPlatformInfo(InContext);
}
else
{
// Grab the source location of the conflicting context
const FManifestContext* ConflictingContext = ExistingEntry->FindContext(InContext.Key, InContext.KeyMetadataObj);
const FString& ExistingEntrySourceLocation = (!ExistingEntryFileName.IsEmpty()) ? ExistingEntryFileName : ConflictingContext->SourceLocation;
UE_LOGFMT(LogLocTextHelper, Warning, "Text conflict{from} for namespace '{locNamespace}' and key '{locKey}'{locKeyMetaData}. First entry is {location}:'{text}'{textMetaData}. Conflicting entry is {conflictLocation}:'{conflictText}'{conflictTextMetaData}.",
("from", (InDescription ? *FString::Printf(TEXT(" from %s"), **InDescription) : TEXT(""))),
("location", *InContext.SourceLocation),
("locNamespace", *SanitizeLogOutput(InNamespace.GetString())),
("locKey", *SanitizeLogOutput(InContext.Key.GetString())),
("locKeyMetaData", (InContext.KeyMetadataObj ? *FString::Printf(TEXT(" (meta-data \"%s\")"), *SanitizeLogOutput(FJsonInternationalizationMetaDataSerializer::MetadataToString(InContext.KeyMetadataObj))) : TEXT(""))),
("text", *SanitizeLogOutput(InSource.Text)),
("textMetaData", (InSource.MetadataObj ? *FString::Printf(TEXT(" (meta-data \"%s\")"), *SanitizeLogOutput(FJsonInternationalizationMetaDataSerializer::MetadataToString(InSource.MetadataObj))) : TEXT(""))),
("conflictLocation", *ExistingEntrySourceLocation),
("conflictText", *SanitizeLogOutput(ExistingEntry->Source.Text)),
("conflictTextMetaData", (ExistingEntry->Source.MetadataObj ? *FString::Printf(TEXT(" (meta-data \"%s\")"), *SanitizeLogOutput(FJsonInternationalizationMetaDataSerializer::MetadataToString(ExistingEntry->Source.MetadataObj))) : TEXT(""))),
("id", LocTextHelper::LocalizationLogIdentifier)
);
ConflictTracker.AddConflict(InNamespace, InContext.Key, InContext.KeyMetadataObj, InSource, InContext.SourceLocation);
ConflictTracker.AddConflict(InNamespace, InContext.Key, InContext.KeyMetadataObj, ExistingEntry->Source, ExistingEntrySourceLocation);
}
}
else
{
bAddSuccessful = Manifest->AddSource(InNamespace, InSource, InContext);
if (!bAddSuccessful)
{
UE_LOGFMT(LogLocTextHelper, Error, "{location}: Failed to add text{from} for namespace '{locNamespace}' and key '{locKey}'{locKeyMetaData} with source '{text}'{textMetaData}.",
("location", *InContext.SourceLocation),
("from", (InDescription ? *FString::Printf(TEXT(" from %s"), **InDescription) : TEXT(""))),
("locNamespace", *InNamespace.GetString()),
("locKey", *InContext.Key.GetString()),
("locKeyMetaData", (InContext.KeyMetadataObj ? *FString::Printf(TEXT(" (meta-data \"%s\")"), *FJsonInternationalizationMetaDataSerializer::MetadataToString(InContext.KeyMetadataObj)) : TEXT(""))),
("text", *InSource.Text),
("textMetaData", (InSource.MetadataObj ? *FString::Printf(TEXT(" (meta-data \"%s\")"), *FJsonInternationalizationMetaDataSerializer::MetadataToString(InSource.MetadataObj)) : TEXT(""))),
("id", LocTextHelper::LocalizationLogIdentifier)
);
}
}
return bAddSuccessful;
}
void FLocTextHelper::UpdateSourceText(const TSharedRef<FManifestEntry>& InOldEntry, TSharedRef<FManifestEntry>& InNewEntry)
{
checkf(Manifest.IsValid(), TEXT("Attempted to update source text, but no manifest has been loaded!"));
Manifest->UpdateEntry(InOldEntry, InNewEntry);
}
TSharedPtr<FManifestEntry> FLocTextHelper::FindSourceText(const FLocKey& InNamespace, const FLocKey& InKey, const FString* InSourceText) const
{
checkf(Manifest.IsValid(), TEXT("Attempted to find source text, but no manifest has been loaded!"));
return Manifest->FindEntryByKey(InNamespace, InKey, InSourceText);
}
TSharedPtr<FManifestEntry> FLocTextHelper::FindSourceText(const FLocKey& InNamespace, const FManifestContext& InContext) const
{
checkf(Manifest.IsValid(), TEXT("Attempted to find source text, but no manifest has been loaded!"));
return Manifest->FindEntryByContext(InNamespace, InContext);
}
void FLocTextHelper::EnumerateSourceTexts(const FEnumerateSourceTextsFuncPtr& InCallback, const bool InCheckDependencies) const
{
checkf(Manifest.IsValid(), TEXT("Attempted to enumerate source texts, but no manifest has been loaded!"));
for (FManifestEntryByStringContainer::TConstIterator It(Manifest->GetEntriesBySourceTextIterator()); It; ++It)
{
const TSharedRef<FManifestEntry> ManifestEntry = It.Value();
bool bShouldEnumerate = true;
if (InCheckDependencies)
{
for (const TSharedPtr<FInternationalizationManifest>& DepManifest : Dependencies)
{
const TSharedPtr<FManifestEntry> DepEntry = DepManifest->FindEntryBySource(ManifestEntry->Namespace, ManifestEntry->Source);
if (DepEntry.IsValid())
{
bShouldEnumerate = false;
break;
}
}
}
if (bShouldEnumerate && !InCallback(ManifestEntry))
{
break;
}
}
}
bool FLocTextHelper::AddTranslation(const FString& InCulture, const FLocKey& InNamespace, const FLocKey& InKey, const TSharedPtr<FLocMetadataObject>& InKeyMetadataObj, const FLocItem& InSource, const FLocItem& InTranslation, const bool InOptional)
{
TSharedPtr<FInternationalizationArchive> Archive = Archives.FindRef(InCulture);
checkf(Archive.IsValid(), TEXT("Attempted to add a translation, but no valid archive could be found for '%s'!"), *InCulture);
return Archive->AddEntry(InNamespace, InKey, InSource, InTranslation, InKeyMetadataObj, InOptional);
}
bool FLocTextHelper::AddTranslation(const FString& InCulture, const TSharedRef<FArchiveEntry>& InEntry)
{
TSharedPtr<FInternationalizationArchive> Archive = Archives.FindRef(InCulture);
checkf(Archive.IsValid(), TEXT("Attempted to add a translation, but no valid archive could be found for '%s'!"), *InCulture);
return Archive->AddEntry(InEntry);
}
bool FLocTextHelper::UpdateTranslation(const FString& InCulture, const FLocKey& InNamespace, const FLocKey& InKey, const TSharedPtr<FLocMetadataObject>& InKeyMetadataObj, const FLocItem& InSource, const FLocItem& InTranslation)
{
TSharedPtr<FInternationalizationArchive> Archive = Archives.FindRef(InCulture);
checkf(Archive.IsValid(), TEXT("Attempted to update a translation, but no valid archive could be found for '%s'!"), *InCulture);
return Archive->SetTranslation(InNamespace, InKey, InSource, InTranslation, InKeyMetadataObj);
}
void FLocTextHelper::UpdateTranslation(const FString& InCulture, const TSharedRef<FArchiveEntry>& InOldEntry, const TSharedRef<FArchiveEntry>& InNewEntry)
{
TSharedPtr<FInternationalizationArchive> Archive = Archives.FindRef(InCulture);
checkf(Archive.IsValid(), TEXT("Attempted to update a translation, but no valid archive could be found for '%s'!"), *InCulture);
Archive->UpdateEntry(InOldEntry, InNewEntry);
}
bool FLocTextHelper::ImportTranslation(const FString& InCulture, const FLocKey& InNamespace, const FLocKey& InKey, const TSharedPtr<FLocMetadataObject> InKeyMetadataObj, const FLocItem& InSource, const FLocItem& InTranslation, const bool InOptional)
{
TSharedPtr<FInternationalizationArchive> Archive = Archives.FindRef(InCulture);
checkf(Archive.IsValid(), TEXT("Attempted to update a translation, but no valid archive could be found for '%s'!"), *InCulture);
// First try and update an existing entry...
if (Archive->SetTranslation(InNamespace, InKey, InSource, InTranslation, InKeyMetadataObj))
{
return true;
}
// ... failing that, try to add a new entry
return Archive->AddEntry(InNamespace, InKey, InSource, InTranslation, InKeyMetadataObj, InOptional);
}
TSharedPtr<FArchiveEntry> FLocTextHelper::FindTranslation(const FString& InCulture, const FLocKey& InNamespace, const FLocKey& InKey, const TSharedPtr<FLocMetadataObject> InKeyMetadataObj) const
{
return FindTranslationImpl(InCulture, InNamespace, InKey, InKeyMetadataObj);
}
void FLocTextHelper::EnumerateTranslations(const FString& InCulture, const FEnumerateTranslationsFuncPtr& InCallback, const bool InCheckDependencies) const
{
TSharedPtr<FInternationalizationArchive> Archive = Archives.FindRef(InCulture);
checkf(Archive.IsValid(), TEXT("Attempted to enumerate translations, but no valid archive could be found for '%s'!"), *InCulture);
EnumerateSourceTexts([&](TSharedRef<FManifestEntry> InManifestEntry) -> bool
{
bool bContinue = true;
for (const FManifestContext& ManifestContext : InManifestEntry->Contexts)
{
TSharedPtr<FArchiveEntry> ArchiveEntry = FindTranslation(InCulture, InManifestEntry->Namespace, ManifestContext.Key, ManifestContext.KeyMetadataObj);
if (ArchiveEntry.IsValid() && !InCallback(ArchiveEntry.ToSharedRef()))
{
bContinue = false;
break;
}
}
return bContinue;
}, InCheckDependencies);
}
void FLocTextHelper::GetExportText(const FString& InCulture, const FLocKey& InNamespace, const FLocKey& InKey, const TSharedPtr<FLocMetadataObject> InKeyMetadataObj, const ELocTextExportSourceMethod InSourceMethod, const FLocItem& InSource, FLocItem& OutSource, FLocItem& OutTranslation) const
{
// Default to the raw source text for the case where we're not using native translations as source
OutSource = InSource;
OutTranslation = FLocItem();
if (InSourceMethod == ELocTextExportSourceMethod::NativeText && !NativeCulture.IsEmpty() && InCulture != NativeCulture)
{
TSharedPtr<FArchiveEntry> NativeArchiveEntry = FindTranslationImpl(NativeCulture, InNamespace, InKey, InKeyMetadataObj);
if (NativeArchiveEntry.IsValid() && !NativeArchiveEntry->Source.IsExactMatch(NativeArchiveEntry->Translation))
{
// Use the native translation as the source
OutSource = NativeArchiveEntry->Translation;
}
}
TSharedPtr<FArchiveEntry> ArchiveEntry = FindTranslationImpl(InCulture, InNamespace, InKey, InKeyMetadataObj);
if (ArchiveEntry.IsValid())
{
// Set the export text to use the current translation if the entry source matches the export source
if (ArchiveEntry->Source.IsExactMatch(OutSource))
{
OutTranslation = ArchiveEntry->Translation;
}
}
// We use the source text as the default translation for the native culture
if (OutTranslation.Text.IsEmpty() && !NativeCulture.IsEmpty() && InCulture == NativeCulture)
{
OutTranslation = OutSource;
}
}
void FLocTextHelper::GetRuntimeText(const FString& InCulture, const FLocKey& InNamespace, const FLocKey& InKey, const TSharedPtr<FLocMetadataObject> InKeyMetadataObj, const ELocTextExportSourceMethod InSourceMethod, const FLocItem& InSource, FLocItem& OutTranslation, const bool bSkipSourceCheck) const
{
OutTranslation = InSource;
TSharedPtr<FArchiveEntry> ArchiveEntry = FindTranslationImpl(InCulture, InNamespace, InKey, InKeyMetadataObj);
if (ArchiveEntry.IsValid() && !ArchiveEntry->Translation.Text.IsEmpty())
{
if (bSkipSourceCheck)
{
// Set the export text to use the current translation
OutTranslation = ArchiveEntry->Translation;
}
else
{
FLocItem ExpectedSource = InSource;
if (InSourceMethod == ELocTextExportSourceMethod::NativeText && !NativeCulture.IsEmpty() && InCulture != NativeCulture)
{
TSharedPtr<FArchiveEntry> NativeArchiveEntry = FindTranslationImpl(NativeCulture, InNamespace, InKey, InKeyMetadataObj);
if (NativeArchiveEntry.IsValid() && !NativeArchiveEntry->Source.IsExactMatch(NativeArchiveEntry->Translation))
{
// Use the native translation as the source
ExpectedSource = NativeArchiveEntry->Translation;
}
}
if (ArchiveEntry->Source.IsExactMatch(ExpectedSource))
{
// Set the export text to use the current translation
OutTranslation = ArchiveEntry->Translation;
}
}
}
}
void FLocTextHelper::AddConflict(const FLocKey& InNamespace, const FLocKey& InKey, const TSharedPtr<FLocMetadataObject>& InKeyMetadata, const FLocItem& InSource, const FString& InSourceLocation)
{
ConflictTracker.AddConflict(InNamespace, InKey, InKeyMetadata, InSource, InSourceLocation);
}
FString FLocTextHelper::GetConflictReport() const
{
return ConflictTracker.GetConflictReport();
}
bool FLocTextHelper::SaveConflictReport(const FString& InReportFilePath, EConflictReportFormat InConflictReportFormat, FText* OutError) const
{
bool bSaved = false;
if (LocFileNotifies.IsValid())
{
LocFileNotifies->PreFileWrite(InReportFilePath);
}
FString ConflictReport;
switch (InConflictReportFormat)
{
case EConflictReportFormat::Txt:
{
ConflictReport = ConflictTracker.GetConflictReport();
break;
}
case EConflictReportFormat::CSV:
{
ConflictReport = ConflictTracker.GetConflictReportAsCSV();
break;
}
default:
{
if (OutError)
{
*OutError = FText::Format(LOCTEXT("Error_UnsupportedConflictReportFormat", "Failed to generate conflict report '{0}'. An unsupported conflict report format was specified. Supported formats can be found in EConflictReportFormat."), FText::FromString(InReportFilePath));
}
return false;
}
}
if (FFileHelper::SaveStringToFile(ConflictReport, *InReportFilePath))
{
bSaved = true;
}
else
{
if (OutError)
{
*OutError = FText::Format(LOCTEXT("Error_SaveConflictReport_SaveStringToFile", "Failed to save conflict report '{0}'."), FText::FromString(InReportFilePath));
}
}
if (LocFileNotifies.IsValid())
{
LocFileNotifies->PostFileWrite(InReportFilePath);
}
return bSaved;
}
FLocTextWordCounts FLocTextHelper::GetWordCountReport(const FDateTime& InTimestamp, const TCHAR* InBaseReportFilePath) const
{
FLocTextWordCounts WordCounts;
// Utility to count the number of words within a string (we use a line-break iterator to avoid counting the whitespace between the words)
TSharedRef<IBreakIterator> LineBreakIterator = FBreakIterator::CreateLineBreakIterator();
auto CountWords = [&LineBreakIterator](const FString& InTextToCount) -> int32
{
int32 NumWords = 0;
LineBreakIterator->SetString(InTextToCount);
int32 PreviousBreak = 0;
int32 CurrentBreak = 0;
while ((CurrentBreak = LineBreakIterator->MoveToNext()) != INDEX_NONE)
{
if (CurrentBreak > PreviousBreak)
{
++NumWords;
}
PreviousBreak = CurrentBreak;
}
LineBreakIterator->ClearString();
return NumWords;
};
// First load in the base report
if (InBaseReportFilePath && FPaths::FileExists(InBaseReportFilePath))
{
FString BaseReportCSV;
if (FFileHelper::LoadFileToString(BaseReportCSV, InBaseReportFilePath))
{
FText BaseReportError;
if (!WordCounts.FromCSV(BaseReportCSV, &BaseReportError))
{
UE_LOGFMT(LogLocTextHelper, Warning, "Failed to parse base word count report '{report}': {reportError}",
("report", InBaseReportFilePath),
("reportError", *BaseReportError.ToString()),
("id", LocTextHelper::LocalizationLogIdentifier)
);
}
}
else
{
UE_LOGFMT(LogLocTextHelper, Warning, "Failed to load base word count report '{report}'.",
("report", InBaseReportFilePath),
("id", LocTextHelper::LocalizationLogIdentifier)
);
}
}
// Then add our new entry (if the last entry in the report has the same timestamp as the one we were given, then replace the data in that entry rather than add a new one)
FLocTextWordCounts::FRowData* WordCountRowData = nullptr;
if (WordCounts.GetRowCount() > 0)
{
FLocTextWordCounts::FRowData* LastRowData = WordCounts.GetRow(WordCounts.GetRowCount() - 1);
check(LastRowData);
if (LastRowData->Timestamp == InTimestamp)
{
WordCountRowData = LastRowData;
WordCountRowData->ResetWordCounts();
}
}
if (!WordCountRowData)
{
WordCountRowData = &WordCounts.AddRow();
WordCountRowData->Timestamp = InTimestamp;
}
// Count the number of source text words
{
TSet<FLocKey> CountedEntries;
EnumerateSourceTexts([&WordCountRowData, &CountedEntries, &CountWords](TSharedRef<FManifestEntry> InManifestEntry) -> bool
{
const int32 NumWords = CountWords(InManifestEntry->Source.Text);
// Gather relevant info from each manifest entry
for (const FManifestContext& Context : InManifestEntry->Contexts)
{
if (!Context.bIsOptional)
{
const FLocKey CountedEntryId = FString::Printf(TEXT("%s::%s::%s"), *InManifestEntry->Source.Text, *InManifestEntry->Namespace.GetString(), *Context.Key.GetString());
if (!CountedEntries.Contains(CountedEntryId))
{
WordCountRowData->SourceWordCount += NumWords;
bool IsAlreadySet = false;
CountedEntries.Add(CountedEntryId, &IsAlreadySet);
check(!IsAlreadySet);
}
}
}
return true; // continue enumeration
}, true);
}
// Count the number of per-culture translation words
for (const FString& CultureName : GetAllCultures())
{
int32& PerCultureWordCount = WordCountRowData->PerCultureWordCounts.Add(CultureName, 0);
TSet<FLocKey> CountedEntries;
// Finds all the manifest entries in the archive and adds the source text word count to the running total if there is a valid translation.
EnumerateSourceTexts([this, &CultureName, &PerCultureWordCount, &CountedEntries, &CountWords](TSharedRef<FManifestEntry> InManifestEntry) -> bool
{
const int32 NumWords = CountWords(InManifestEntry->Source.Text);
// Gather relevant info from each manifest entry
for (const FManifestContext& Context : InManifestEntry->Contexts)
{
if (!Context.bIsOptional)
{
// Use the exported text when counting as it will take native translations into account
FLocItem WordCountSource;
FLocItem WordCountTranslation;
GetExportText(CultureName, InManifestEntry->Namespace, Context.Key, Context.KeyMetadataObj, ELocTextExportSourceMethod::NativeText, InManifestEntry->Source, WordCountSource, WordCountTranslation);
if (!WordCountTranslation.Text.IsEmpty())
{
const FLocKey CountedEntryId = FString::Printf(TEXT("%s::%s::%s"), *InManifestEntry->Source.Text, *InManifestEntry->Namespace.GetString(), *Context.Key.GetString());
if (!CountedEntries.Contains(CountedEntryId))
{
PerCultureWordCount += NumWords;
bool IsAlreadySet = false;
CountedEntries.Add(CountedEntryId, &IsAlreadySet);
check(!IsAlreadySet);
}
}
}
}
return true; // continue enumeration
}, true);
}
return WordCounts;
}
bool FLocTextHelper::SaveWordCountReport(const FDateTime& InTimestamp, const FString& InReportFilePath, FText* OutError) const
{
bool bSaved = false;
if (LocFileNotifies.IsValid())
{
LocFileNotifies->PreFileWrite(InReportFilePath);
}
FLocTextWordCounts WordCounts = GetWordCountReport(InTimestamp, *InReportFilePath);
WordCounts.TrimReport();
const FString WordCountReportCSV = WordCounts.ToCSV();
if (FFileHelper::SaveStringToFile(WordCountReportCSV, *InReportFilePath))
{
bSaved = true;
}
else
{
if (OutError)
{
*OutError = FText::Format(LOCTEXT("Error_SaveWordCountReport_SaveStringToFile", "Failed to save word count report '{0}'."), FText::FromString(InReportFilePath));
}
}
if (LocFileNotifies.IsValid())
{
LocFileNotifies->PostFileWrite(InReportFilePath);
}
return bSaved;
}
FString FLocTextHelper::SanitizeLogOutput(FStringView InString)
{
return SanitizeLogOutput(FString(InString));
}
FString FLocTextHelper::SanitizeLogOutput(FString&& ResultStr)
{
if (ResultStr.IsEmpty())
{
return MoveTemp(ResultStr);
}
ResultStr.ReplaceCharWithEscapedCharInline();
if (!GIsBuildMachine)
{
return MoveTemp(ResultStr);
}
static const TCHAR* ErrorStrs[] = {
TEXT("Error"),
TEXT("Failed"),
TEXT("[BEROR]"),
TEXT("Utility finished with exit code: -1"),
TEXT("is not recognized as an internal or external command"),
TEXT("Could not open solution: "),
TEXT("Parameter format not correct"),
TEXT("Another build is already started on this computer."),
TEXT("Sorry but the link was not completed because memory was exhausted."),
TEXT("simply rerunning the compiler might fix this problem"),
TEXT("No connection could be made because the target machine actively refused"),
TEXT("Internal Linker Exception:"),
TEXT(": warning LNK4019: corrupt string table"),
TEXT("Proxy could not update its cache"),
TEXT("You have not agreed to the Xcode license agreements"),
TEXT("Connection to build service terminated"),
TEXT("cannot execute binary file"),
TEXT("Invalid solution configuration"),
TEXT("is from a previous version of this application and must be converted in order to build"),
TEXT("This computer has not been authenticated for your account using Steam Guard"),
TEXT("invalid name for SPA section"),
TEXT(": Invalid file name, "),
TEXT("The specified PFX file do not exist. Aborting"),
TEXT("binary is not found. Aborting"),
TEXT("Input file not found: "),
TEXT("An exception occurred during merging:"),
TEXT("Install the 'Microsoft Windows SDK for Windows 7 and .NET Framework 3.5 SP1'"),
TEXT("is less than package's new version 0x"),
TEXT("current engine version is older than version the package was originally saved with"),
TEXT("exceeds maximum length"),
TEXT("can't edit exclusive file already opened"),
};
static const TArray<FString> ReplacementStrings = []()
{
TArray<FString> TmpReplacementStrings;
TmpReplacementStrings.Reserve(UE_ARRAY_COUNT(ErrorStrs));
for (const TCHAR* ErrorStr : ErrorStrs)
{
FString ReplacementStr = TmpReplacementStrings.Add_GetRef(ErrorStr);
ReplacementStr.InsertAt(1, TEXT(' '));
}
return TmpReplacementStrings;
}();
check(UE_ARRAY_COUNT(ErrorStrs) == ReplacementStrings.Num());
for (int32 ErrorIndex = 0; ErrorIndex < UE_ARRAY_COUNT(ErrorStrs); ++ErrorIndex)
{
ResultStr.ReplaceInline(ErrorStrs[ErrorIndex], *ReplacementStrings[ErrorIndex]);
}
return MoveTemp(ResultStr);
}
bool FLocTextHelper::FindKeysForLegacyTranslation(const FString& InCulture, const FLocKey& InNamespace, const FString& InSource, const TSharedPtr<FLocMetadataObject> InKeyMetadataObj, TArray<FLocKey>& OutKeys) const
{
checkf(Manifest.IsValid(), TEXT("Attempted to find a key for a legacy translation, but no manifest has been loaded!"));
TSharedPtr<FInternationalizationArchive> NativeArchive;
if (!NativeCulture.IsEmpty() && InCulture != NativeCulture)
{
NativeArchive = Archives.FindRef(NativeCulture);
checkf(NativeArchive.IsValid(), TEXT("Attempted to find a key for a legacy translation, but no valid archive could be found for '%s'!"), *NativeCulture);
}
return FindKeysForLegacyTranslation(Manifest.ToSharedRef(), NativeArchive, InNamespace, InSource, InKeyMetadataObj, OutKeys);
}
bool FLocTextHelper::FindKeysForLegacyTranslation(const TSharedRef<const FInternationalizationManifest>& InManifest, const TSharedPtr<const FInternationalizationArchive>& InNativeArchive, const FLocKey& InNamespace, const FString& InSource, const TSharedPtr<FLocMetadataObject> InKeyMetadataObj, TArray<FLocKey>& OutKeys)
{
FString RealSourceText = InSource;
// The source text may be a native translation, so we first need to check the native archive to find the real source text that will exist in the manifest
if (InNativeArchive.IsValid())
{
// We don't maintain a translation -> source mapping, so we just have to brute force it
for (FArchiveEntryByStringContainer::TConstIterator It = InNativeArchive->GetEntriesBySourceTextIterator(); It; ++It)
{
const TSharedRef<FArchiveEntry>& ArchiveEntry = It.Value();
if (ArchiveEntry->Namespace == InNamespace && ArchiveEntry->Translation.Text.Equals(InSource, ESearchCase::CaseSensitive))
{
if (!ArchiveEntry->KeyMetadataObj.IsValid() && !InKeyMetadataObj.IsValid())
{
RealSourceText = ArchiveEntry->Source.Text;
break;
}
else if ((InKeyMetadataObj.IsValid() != ArchiveEntry->KeyMetadataObj.IsValid()))
{
// If we are in here, we know that one of the metadata entries is null, if the other contains zero entries we will still consider them equivalent.
if ((InKeyMetadataObj.IsValid() && InKeyMetadataObj->Values.Num() == 0) || (ArchiveEntry->KeyMetadataObj.IsValid() && ArchiveEntry->KeyMetadataObj->Values.Num() == 0))
{
RealSourceText = ArchiveEntry->Source.Text;
break;
}
}
else if (*ArchiveEntry->KeyMetadataObj == *InKeyMetadataObj)
{
RealSourceText = ArchiveEntry->Source.Text;
break;
}
}
}
}
bool bFoundKeys = false;
TSharedPtr<FManifestEntry> ManifestEntry = InManifest->FindEntryBySource(InNamespace, FLocItem(RealSourceText));
if (ManifestEntry.IsValid())
{
for (const FManifestContext& Context : ManifestEntry->Contexts)
{
if (Context.KeyMetadataObj.IsValid() != InKeyMetadataObj.IsValid())
{
continue;
}
else if ((!Context.KeyMetadataObj.IsValid() && !InKeyMetadataObj.IsValid()) || (*Context.KeyMetadataObj == *InKeyMetadataObj))
{
OutKeys.AddUnique(Context.Key);
bFoundKeys = true;
}
}
}
return bFoundKeys;
}
TSharedPtr<FInternationalizationManifest> FLocTextHelper::LoadManifestImpl(const FString& InManifestFilePath, const ELocTextHelperLoadFlags InLoadFlags, FText* OutError)
{
TSharedRef<FInternationalizationManifest> LocalManifest = MakeShared<FInternationalizationManifest>();
auto LoadSingleManifest = [this, &LocalManifest, &OutError](const FString& InManifestFilePathToLoad, const FName InPlatformName) -> bool
{
bool bLoaded = false;
if (LocFileNotifies.IsValid())
{
LocFileNotifies->PreFileRead(InManifestFilePathToLoad);
}
if (FJsonInternationalizationManifestSerializer::DeserializeManifestFromFile(InManifestFilePathToLoad, LocalManifest, InPlatformName))
{
bLoaded = true;
}
else if (OutError)
{
*OutError = FText::Format(LOCTEXT("Error_LoadManifest_DeserializeFile", "Failed to deserialize manifest '{0}'."), FText::FromString(InManifestFilePathToLoad));
}
if (LocFileNotifies.IsValid())
{
LocFileNotifies->PostFileRead(InManifestFilePathToLoad);
}
return bLoaded;
};
// Attempt to load an existing manifest first
if (EnumHasAnyFlags(InLoadFlags, ELocTextHelperLoadFlags::Load))
{
const bool bExists = FPaths::FileExists(InManifestFilePath);
if (bExists)
{
bool bLoadedAll = true;
bLoadedAll &= LoadSingleManifest(InManifestFilePath, FName());
{
// Load all per-platform manifests too
// We always do this, as we may have changed the split config so don't want to lose data
const FString PlatformManifestName = FPaths::GetCleanFilename(InManifestFilePath);
const FString PlatformLocalizationPath = FPaths::GetPath(InManifestFilePath) / FPaths::GetPlatformLocalizationFolderName();
IFileManager::Get().IterateDirectory(*PlatformLocalizationPath, [&](const TCHAR* FilenameOrDirectory, bool bIsDirectory) -> bool
{
if (bIsDirectory)
{
const FString PlatformManifestFilePath = FilenameOrDirectory / PlatformManifestName;
if (FPaths::FileExists(PlatformManifestFilePath))
{
const FString SplitPlatformName = FPaths::GetCleanFilename(FilenameOrDirectory);
bLoadedAll &= LoadSingleManifest(PlatformManifestFilePath, *SplitPlatformName);
}
}
return true;
});
}
if (bLoadedAll)
{
return LocalManifest;
}
else
{
// Don't allow fallback to Create if the file exists but could not be loaded
return nullptr;
}
}
}
// If we're allowed to create a manifest than we can never fail (unless file is corrupt above)
if (EnumHasAnyFlags(InLoadFlags, ELocTextHelperLoadFlags::Create))
{
return LocalManifest;
}
if (OutError)
{
if (!EnumHasAnyFlags(InLoadFlags, ELocTextHelperLoadFlags::Load))
{
*OutError = LOCTEXT("Error_LoadManifest_CallerDidNotSpecifyLoadOrCreate", "Caller did not specify either Load or Create.");
}
else
{
*OutError = FText::Format(LOCTEXT("Error_LoadManifest_MissingManifest", "Manifest '{0}' does not exist."), FText::FromString(InManifestFilePath));
}
}
return nullptr;
}
bool FLocTextHelper::SaveManifestImpl(const TSharedRef<const FInternationalizationManifest>& InManifest, const FString& InManifestFilePath, FText* OutError) const
{
auto SaveSingleManifest = [this, &OutError](const TSharedRef<const FInternationalizationManifest>& InManifestToSave, const FString& InManifestFilePathToSave) -> bool
{
bool bSaved = false;
if (LocFileNotifies.IsValid())
{
LocFileNotifies->PreFileWrite(InManifestFilePathToSave);
}
if (FJsonInternationalizationManifestSerializer::SerializeManifestToFile(InManifestToSave, InManifestFilePathToSave))
{
bSaved = true;
}
else
{
if (OutError)
{
*OutError = FText::Format(LOCTEXT("Error_SaveManifest_SerializeFile", "Failed to serialize manifest '{0}'."), FText::FromString(InManifestFilePathToSave));
}
}
if (LocFileNotifies.IsValid())
{
LocFileNotifies->PostFileWrite(InManifestFilePathToSave);
}
return bSaved;
};
bool bSavedAll = true;
if (ShouldSplitPlatformData())
{
const FString PlatformManifestName = FPaths::GetCleanFilename(InManifestFilePath);
const FString PlatformLocalizationPath = FPaths::GetPath(InManifestFilePath) / FPaths::GetPlatformLocalizationFolderName();
// Split the manifest into separate entries for each platform, as well as a platform agnostic manifest
TSharedRef<FInternationalizationManifest> PlatformAgnosticManifest = MakeShared<FInternationalizationManifest>();
TMap<FName, TSharedRef<FInternationalizationManifest>> PerPlatformManifests;
{
// Always add the split platforms so that they generate an empty manifest if there are no entries for that platform in the platform agnostic manifest
for (const FString& SplitPlatformName : GetPlatformsToSplit())
{
PerPlatformManifests.Add(*SplitPlatformName, MakeShared<FInternationalizationManifest>());
}
// Split the manifest entries based on the platform they belonged to
for (FManifestEntryByStringContainer::TConstIterator It(InManifest->GetEntriesBySourceTextIterator()); It; ++It)
{
const TSharedRef<FManifestEntry> ManifestEntry = It.Value();
for (const FManifestContext& Context : ManifestEntry->Contexts)
{
TSharedPtr<FInternationalizationManifest> ManifestToUpdate = PlatformAgnosticManifest;
if (!Context.PlatformName.IsNone())
{
if (TSharedRef<FInternationalizationManifest>* PerPlatformManifest = PerPlatformManifests.Find(Context.PlatformName))
{
ManifestToUpdate = *PerPlatformManifest;
}
}
check(ManifestToUpdate.IsValid());
if (!ManifestToUpdate->AddSource(ManifestEntry->Namespace, ManifestEntry->Source, Context))
{
UE_LOGFMT(LogLocTextHelper, Error, "Failed to add text for namespace '{locNamespace}' and key '{locKey}'{locKeyMetaData} with source '{text}'{textMetaData}{forPlatform}.",
("locNamespace", *ManifestEntry->Namespace.GetString()),
("locKey", *Context.Key.GetString()),
("locKeyMetaData", (Context.KeyMetadataObj ? *FString::Printf(TEXT(" (meta-data \"%s\")"), *FJsonInternationalizationMetaDataSerializer::MetadataToString(Context.KeyMetadataObj)) : TEXT(""))),
("text", *ManifestEntry->Source.Text),
("textMetaData", (ManifestEntry->Source.MetadataObj ? *FString::Printf(TEXT(" (meta-data \"%s\")"), *FJsonInternationalizationMetaDataSerializer::MetadataToString(ManifestEntry->Source.MetadataObj)) : TEXT(""))),
("forPlatform", (Context.PlatformName.IsNone() ? TEXT("") : *FString::Printf(TEXT(" for platform \"%s\""), *Context.PlatformName.ToString()))),
("id", LocTextHelper::LocalizationLogIdentifier)
);
}
}
}
}
bSavedAll &= SaveSingleManifest(PlatformAgnosticManifest, InManifestFilePath);
for (const auto& PerPlatformManifestPair : PerPlatformManifests)
{
const FString PlatformManifestFilePath = PlatformLocalizationPath / PerPlatformManifestPair.Key.ToString() / PlatformManifestName;
bSavedAll &= SaveSingleManifest(PerPlatformManifestPair.Value, PlatformManifestFilePath);
}
}
else
{
bSavedAll &= SaveSingleManifest(InManifest, InManifestFilePath);
}
return bSavedAll;
}
TSharedPtr<FInternationalizationArchive> FLocTextHelper::LoadArchiveImpl(const FString& InArchiveFilePath, const ELocTextHelperLoadFlags InLoadFlags, FText* OutError)
{
TSharedRef<FInternationalizationArchive> LocalArchive = MakeShared<FInternationalizationArchive>();
auto LoadSingleArchive = [this, &LocalArchive, &OutError](const FString& InArchiveFilePathToLoad) -> bool
{
bool bLoaded = false;
if (LocFileNotifies.IsValid())
{
LocFileNotifies->PreFileRead(InArchiveFilePathToLoad);
}
TSharedPtr<FInternationalizationArchive> NativeArchive;
if (!NativeCulture.IsEmpty())
{
NativeArchive = Archives.FindRef(NativeCulture);
}
if (FJsonInternationalizationArchiveSerializer::DeserializeArchiveFromFile(InArchiveFilePathToLoad, LocalArchive, Manifest, NativeArchive))
{
bLoaded = true;
}
else
{
if (OutError)
{
*OutError = FText::Format(LOCTEXT("Error_LoadArchive_DeserializeFile", "Failed to deserialize archive '{0}'."), FText::FromString(InArchiveFilePathToLoad));
}
}
if (LocFileNotifies.IsValid())
{
LocFileNotifies->PostFileRead(InArchiveFilePathToLoad);
}
return bLoaded;
};
// Attempt to load an existing archive first
if (EnumHasAnyFlags(InLoadFlags, ELocTextHelperLoadFlags::Load))
{
const bool bExists = FPaths::FileExists(InArchiveFilePath);
if (bExists)
{
bool bLoadedAll = true;
bLoadedAll &= LoadSingleArchive(InArchiveFilePath);
{
// Load all per-platform archives too
// We always do this, as we may have changed the split config so don't want to lose data
const FString ArchiveCultureFilePath = FPaths::GetPath(InArchiveFilePath);
const FString PlatformArchiveName = FPaths::GetCleanFilename(InArchiveFilePath);
const FString PlatformArchiveCulture = FPaths::GetCleanFilename(ArchiveCultureFilePath);
const FString PlatformLocalizationPath = FPaths::GetPath(ArchiveCultureFilePath) / FPaths::GetPlatformLocalizationFolderName();
IFileManager::Get().IterateDirectory(*PlatformLocalizationPath, [&](const TCHAR* FilenameOrDirectory, bool bIsDirectory) -> bool
{
if (bIsDirectory)
{
const FString PlatformArchiveFilePath = FilenameOrDirectory / PlatformArchiveCulture / PlatformArchiveName;
if (FPaths::FileExists(PlatformArchiveFilePath))
{
bLoadedAll &= LoadSingleArchive(PlatformArchiveFilePath);
}
}
return true;
});
}
if (bLoadedAll)
{
return LocalArchive;
}
else
{
// Don't allow fallback to Create if the file exists but could not be loaded
return nullptr;
}
}
}
// If we're allowed to create an archive than we can never fail (unless file is corrupt above)
if (EnumHasAnyFlags(InLoadFlags, ELocTextHelperLoadFlags::Create))
{
return LocalArchive;
}
if (OutError)
{
if (!EnumHasAnyFlags(InLoadFlags, ELocTextHelperLoadFlags::Load))
{
*OutError = LOCTEXT("Error_LoadManifest_CallerDidNotSpecifyLoadOrCreate", "Caller did not specify either Load or Create.");
}
else
{
*OutError = FText::Format(LOCTEXT("Error_LoadArchive_MissingArchive", "Archive '{0}' does not exist."), FText::FromString(InArchiveFilePath));
}
}
return nullptr;
}
bool FLocTextHelper::SaveArchiveImpl(const TSharedRef<const FInternationalizationArchive>& InArchive, const FString& InArchiveFilePath, FText* OutError) const
{
auto SaveSingleArchive = [this, &OutError](const TSharedRef<const FInternationalizationArchive>& InArchiveToSave, const FString& InArchiveFilePathToSave) -> bool
{
bool bSaved = false;
if (LocFileNotifies.IsValid())
{
LocFileNotifies->PreFileWrite(InArchiveFilePathToSave);
}
if (FJsonInternationalizationArchiveSerializer::SerializeArchiveToFile(InArchiveToSave, InArchiveFilePathToSave))
{
bSaved = true;
}
else
{
if (OutError)
{
*OutError = FText::Format(LOCTEXT("Error_SaveArchive_SerializeFile", "Failed to serialize archive '{0}'."), FText::FromString(InArchiveFilePathToSave));
}
}
if (LocFileNotifies.IsValid())
{
LocFileNotifies->PostFileWrite(InArchiveFilePathToSave);
}
return bSaved;
};
bool bSavedAll = true;
if (ShouldSplitPlatformData())
{
const FString ArchiveCultureFilePath = FPaths::GetPath(InArchiveFilePath);
const FString PlatformArchiveName = FPaths::GetCleanFilename(InArchiveFilePath);
const FString PlatformArchiveCulture = FPaths::GetCleanFilename(ArchiveCultureFilePath);
const FString PlatformLocalizationPath = FPaths::GetPath(ArchiveCultureFilePath) / FPaths::GetPlatformLocalizationFolderName();
// Split the archive into separate entries for each platform, as well as a platform agnostic archive
TSharedRef<FInternationalizationArchive> PlatformAgnosticArchive = MakeShared<FInternationalizationArchive>();
TMap<FName, TSharedRef<FInternationalizationArchive>> PerPlatformArchives;
{
// Always add the split platforms so that they generate an empty archives if there are no entries for that platform in the platform agnostic archive
for (const FString& SplitPlatformName : GetPlatformsToSplit())
{
PerPlatformArchives.Add(*SplitPlatformName, MakeShared<FInternationalizationArchive>());
}
// Split the archive entries based on the platform they belonged to
EnumerateSourceTexts([&InArchive, &PlatformAgnosticArchive, &PerPlatformArchives](TSharedRef<FManifestEntry> InManifestEntry) -> bool
{
for (const FManifestContext& Context : InManifestEntry->Contexts)
{
TSharedPtr<FInternationalizationArchive> ArchiveToUpdate = PlatformAgnosticArchive;
if (!Context.PlatformName.IsNone())
{
if (TSharedRef<FInternationalizationArchive>* PerPlatformArchive = PerPlatformArchives.Find(Context.PlatformName))
{
ArchiveToUpdate = *PerPlatformArchive;
}
}
check(ArchiveToUpdate.IsValid());
// Keep any translation for the source text
TSharedPtr<FArchiveEntry> ArchiveEntry = InArchive->FindEntryByKey(InManifestEntry->Namespace, Context.Key, Context.KeyMetadataObj);
if (ArchiveEntry.IsValid())
{
ArchiveToUpdate->AddEntry(ArchiveEntry.ToSharedRef());
}
}
return true; // continue enumeration
}, true);
}
bSavedAll &= SaveSingleArchive(PlatformAgnosticArchive, InArchiveFilePath);
for (const auto& PerPlatformArchivePair : PerPlatformArchives)
{
const FString PlatformArchiveFilePath = PlatformLocalizationPath / PerPlatformArchivePair.Key.ToString() / PlatformArchiveCulture / PlatformArchiveName;
bSavedAll &= SaveSingleArchive(PerPlatformArchivePair.Value, PlatformArchiveFilePath);
}
}
else
{
bSavedAll &= SaveSingleArchive(InArchive, InArchiveFilePath);
}
return bSavedAll;
}
TSharedPtr<FArchiveEntry> FLocTextHelper::FindTranslationImpl(const FString& InCulture, const FLocKey& InNamespace, const FLocKey& InKey, const TSharedPtr<FLocMetadataObject> InKeyMetadataObj) const
{
TSharedPtr<FInternationalizationArchive> Archive = Archives.FindRef(InCulture);
checkf(Archive.IsValid(), TEXT("Attempted to find a translation, but no valid archive could be found for '%s'!"), *InCulture);
return Archive->FindEntryByKey(InNamespace, InKey, InKeyMetadataObj);
}
#undef LOCTEXT_NAMESPACE