307 lines
12 KiB
C++
307 lines
12 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "Commandlets/ImportDialogueScriptCommandlet.h"
|
|
|
|
#include "Commandlets/Commandlet.h"
|
|
#include "Commandlets/ExportDialogueScriptCommandlet.h"
|
|
#include "Containers/Array.h"
|
|
#include "Containers/Map.h"
|
|
#include "CoreTypes.h"
|
|
#include "Internationalization/InternationalizationManifest.h"
|
|
#include "Internationalization/Text.h"
|
|
#include "LocTextHelper.h"
|
|
#include "LocalizationSourceControlUtil.h"
|
|
#include "Logging/LogCategory.h"
|
|
#include "Logging/LogMacros.h"
|
|
#include "Misc/AssertionMacros.h"
|
|
#include "Misc/CString.h"
|
|
#include "Misc/FileHelper.h"
|
|
#include "Serialization/Csv/CsvParser.h"
|
|
#include "Sound/DialogueWave.h"
|
|
#include "Templates/SharedPointer.h"
|
|
#include "Trace/Detail/Channel.h"
|
|
#include "UObject/Class.h"
|
|
#include "UObject/PropertyPortFlags.h"
|
|
#include "UObject/UnrealType.h"
|
|
|
|
DEFINE_LOG_CATEGORY_STATIC(LogImportDialogueScriptCommandlet, Log, All);
|
|
|
|
UImportDialogueScriptCommandlet::UImportDialogueScriptCommandlet(const FObjectInitializer& ObjectInitializer)
|
|
: Super(ObjectInitializer)
|
|
{
|
|
}
|
|
|
|
int32 UImportDialogueScriptCommandlet::Main(const FString& Params)
|
|
{
|
|
// Parse command line
|
|
TArray<FString> Tokens;
|
|
TArray<FString> Switches;
|
|
TMap<FString, FString> ParamVals;
|
|
UCommandlet::ParseCommandLine(*Params, Tokens, Switches, ParamVals);
|
|
|
|
// Set config path
|
|
FString ConfigPath;
|
|
{
|
|
const FString* ConfigPathParamVal = ParamVals.Find(FString(TEXT("Config")));
|
|
if (!ConfigPathParamVal)
|
|
{
|
|
UE_LOG(LogImportDialogueScriptCommandlet, Error, TEXT("No config specified."));
|
|
return -1;
|
|
}
|
|
ConfigPath = *ConfigPathParamVal;
|
|
}
|
|
|
|
// Set config section
|
|
FString SectionName;
|
|
{
|
|
const FString* SectionNameParamVal = ParamVals.Find(FString(TEXT("Section")));
|
|
if (!SectionNameParamVal)
|
|
{
|
|
UE_LOG(LogImportDialogueScriptCommandlet, Error, TEXT("No config section specified."));
|
|
return -1;
|
|
}
|
|
SectionName = *SectionNameParamVal;
|
|
}
|
|
|
|
// Source path to the root folder that dialogue script CSV files live in
|
|
FString SourcePath;
|
|
if (!GetPathFromConfig(*SectionName, TEXT("SourcePath"), SourcePath, ConfigPath))
|
|
{
|
|
UE_LOG(LogImportDialogueScriptCommandlet, Error, TEXT("No source path specified."));
|
|
return -1;
|
|
}
|
|
|
|
// Destination path to the root folder that manifest/archive files live in
|
|
FString DestinationPath;
|
|
if (!GetPathFromConfig(*SectionName, TEXT("DestinationPath"), DestinationPath, ConfigPath))
|
|
{
|
|
UE_LOG(LogImportDialogueScriptCommandlet, Error, TEXT("No destination path specified."));
|
|
return -1;
|
|
}
|
|
|
|
// Get culture directory setting, default to true if not specified (used to allow picking of export directory with windows file dialog from Translation Editor)
|
|
bool bUseCultureDirectory = true;
|
|
if (!GetBoolFromConfig(*SectionName, TEXT("bUseCultureDirectory"), bUseCultureDirectory, ConfigPath))
|
|
{
|
|
bUseCultureDirectory = true;
|
|
}
|
|
|
|
// Get the native culture
|
|
FString NativeCulture;
|
|
if (!GetStringFromConfig(*SectionName, TEXT("NativeCulture"), NativeCulture, ConfigPath))
|
|
{
|
|
UE_LOG(LogImportDialogueScriptCommandlet, Error, TEXT("No native culture specified."));
|
|
return -1;
|
|
}
|
|
|
|
// Get cultures to generate
|
|
TArray<FString> CulturesToGenerate;
|
|
if (GetStringArrayFromConfig(*SectionName, TEXT("CulturesToGenerate"), CulturesToGenerate, ConfigPath) == 0)
|
|
{
|
|
UE_LOG(LogImportDialogueScriptCommandlet, Error, TEXT("No cultures specified for import."));
|
|
return -1;
|
|
}
|
|
|
|
// Get the manifest name
|
|
FString ManifestName;
|
|
if (!GetStringFromConfig(*SectionName, TEXT("ManifestName"), ManifestName, ConfigPath))
|
|
{
|
|
UE_LOG(LogImportDialogueScriptCommandlet, Error, TEXT("No manifest name specified."));
|
|
return -1;
|
|
}
|
|
|
|
// Get the archive name
|
|
FString ArchiveName;
|
|
if (!GetStringFromConfig(*SectionName, TEXT("ArchiveName"), ArchiveName, ConfigPath))
|
|
{
|
|
UE_LOG(LogImportDialogueScriptCommandlet, Error, TEXT("No archive name specified."));
|
|
return -1;
|
|
}
|
|
|
|
// Get the dialogue script name
|
|
FString DialogueScriptName;
|
|
if (!GetStringFromConfig(*SectionName, TEXT("DialogueScriptName"), DialogueScriptName, ConfigPath))
|
|
{
|
|
UE_LOG(LogImportDialogueScriptCommandlet, Error, TEXT("No dialogue script name specified."));
|
|
return -1;
|
|
}
|
|
|
|
// We may only have a single culture if using this setting
|
|
if (!bUseCultureDirectory && CulturesToGenerate.Num() > 1)
|
|
{
|
|
UE_LOG(LogImportDialogueScriptCommandlet, Error, TEXT("bUseCultureDirectory may only be used with a single culture."));
|
|
return false;
|
|
}
|
|
|
|
// Load the manifest and all archives
|
|
FLocTextHelper LocTextHelper(DestinationPath, ManifestName, ArchiveName, NativeCulture, CulturesToGenerate, GatherManifestHelper->GetLocFileNotifies(), GatherManifestHelper->GetPlatformSplitMode());
|
|
LocTextHelper.SetCopyrightNotice(GatherManifestHelper->GetCopyrightNotice());
|
|
{
|
|
FText LoadError;
|
|
if (!LocTextHelper.LoadAll(ELocTextHelperLoadFlags::LoadOrCreate, &LoadError))
|
|
{
|
|
UE_LOG(LogImportDialogueScriptCommandlet, Error, TEXT("%s"), *LoadError.ToString());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Import the native culture first as this may trigger additional translations in foreign archives
|
|
{
|
|
const FString CultureSourcePath = SourcePath / (bUseCultureDirectory ? NativeCulture : TEXT(""));
|
|
const FString CultureDestinationPath = DestinationPath / NativeCulture;
|
|
ImportDialogueScriptForCulture(LocTextHelper, CultureSourcePath / DialogueScriptName, NativeCulture, true);
|
|
}
|
|
|
|
// Import any remaining cultures
|
|
for (const FString& CultureName : CulturesToGenerate)
|
|
{
|
|
// Skip the native culture as we already processed it above
|
|
if (CultureName == NativeCulture)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
const FString CultureSourcePath = SourcePath / (bUseCultureDirectory ? CultureName : TEXT(""));
|
|
const FString CultureDestinationPath = DestinationPath / CultureName;
|
|
ImportDialogueScriptForCulture(LocTextHelper, CultureSourcePath / DialogueScriptName, CultureName, false);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
bool UImportDialogueScriptCommandlet::ImportDialogueScriptForCulture(FLocTextHelper& InLocTextHelper, const FString& InDialogueScriptFileName, const FString& InCultureName, const bool bIsNativeCulture)
|
|
{
|
|
// Load dialogue script file contents to string
|
|
FString DialogScriptFileContents;
|
|
if (!FFileHelper::LoadFileToString(DialogScriptFileContents, *InDialogueScriptFileName))
|
|
{
|
|
UE_LOG(LogImportDialogueScriptCommandlet, Error, TEXT("Failed to load contents of dialog script file '%s' for culture '%s'."), *InDialogueScriptFileName, *InCultureName);
|
|
return false;
|
|
}
|
|
|
|
// Parse dialogue script file contents
|
|
const FCsvParser DialogScriptFileParser(DialogScriptFileContents);
|
|
const FCsvParser::FRows& Rows = DialogScriptFileParser.GetRows();
|
|
|
|
// Validate dialogue script row count
|
|
if (Rows.Num() <= 0)
|
|
{
|
|
UE_LOG(LogImportDialogueScriptCommandlet, Error, TEXT("Dialogue script file has insufficient rows for culture '%s'. Expected at least 1 row, got %d."), *InCultureName, Rows.Num());
|
|
return false;
|
|
}
|
|
|
|
const FProperty* SpokenDialogueProperty = FDialogueScriptEntry::StaticStruct()->FindPropertyByName(GET_MEMBER_NAME_CHECKED(FDialogueScriptEntry, SpokenDialogue));
|
|
const FProperty* LocalizationKeysProperty = FDialogueScriptEntry::StaticStruct()->FindPropertyByName(GET_MEMBER_NAME_CHECKED(FDialogueScriptEntry, LocalizationKeys));
|
|
|
|
// We need the SpokenDialogue and LocalizationKeys properties in order to perform the import, so find their respective columns in the CSV data
|
|
int32 SpokenDialogueColumnIndex = INDEX_NONE;
|
|
int32 LocalizationKeysColumnIndex = INDEX_NONE;
|
|
{
|
|
const FString SpokenDialogueColumnName = SpokenDialogueProperty->GetName();
|
|
const FString LocalizationKeysColumnName = LocalizationKeysProperty->GetName();
|
|
|
|
const auto& HeaderRowData = Rows[0];
|
|
for (int32 ColumnIndex = 0; ColumnIndex < HeaderRowData.Num(); ++ColumnIndex)
|
|
{
|
|
const TCHAR* const CellData = HeaderRowData[ColumnIndex];
|
|
if (FCString::Stricmp(CellData, *SpokenDialogueColumnName) == 0)
|
|
{
|
|
SpokenDialogueColumnIndex = ColumnIndex;
|
|
}
|
|
else if (FCString::Stricmp(CellData, *LocalizationKeysColumnName) == 0)
|
|
{
|
|
LocalizationKeysColumnIndex = ColumnIndex;
|
|
}
|
|
|
|
if (SpokenDialogueColumnIndex != INDEX_NONE && LocalizationKeysColumnIndex != INDEX_NONE)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (SpokenDialogueColumnIndex == INDEX_NONE)
|
|
{
|
|
UE_LOG(LogImportDialogueScriptCommandlet, Error, TEXT("Dialogue script file is missing the required column '%s' for culture '%s'."), *SpokenDialogueProperty->GetName(), *InCultureName);
|
|
return false;
|
|
}
|
|
|
|
if (LocalizationKeysColumnIndex == INDEX_NONE)
|
|
{
|
|
UE_LOG(LogImportDialogueScriptCommandlet, Error, TEXT("Dialogue script file is missing the required column '%s' for culture '%s'."), *LocalizationKeysProperty->GetName(), *InCultureName);
|
|
return false;
|
|
}
|
|
|
|
bool bHasUpdatedArchive = false;
|
|
|
|
// Parse each row of the CSV data
|
|
for (int32 RowIndex = 1; RowIndex < Rows.Num(); ++RowIndex)
|
|
{
|
|
const auto& RowData = Rows[RowIndex];
|
|
|
|
FDialogueScriptEntry ParsedScriptEntry;
|
|
|
|
// Parse the SpokenDialogue data
|
|
{
|
|
const TCHAR* const CellData = RowData[SpokenDialogueColumnIndex];
|
|
if (SpokenDialogueProperty->ImportText_InContainer(CellData, &ParsedScriptEntry, nullptr, PPF_None) == nullptr)
|
|
{
|
|
UE_LOG(LogImportDialogueScriptCommandlet, Error, TEXT("Failed to parse the required column '%s' for row '%d' for culture '%s'."), *SpokenDialogueProperty->GetName(), RowIndex, *InCultureName);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Parse the LocalizationKeys data
|
|
{
|
|
const TCHAR* const CellData = RowData[LocalizationKeysColumnIndex];
|
|
if (LocalizationKeysProperty->ImportText_InContainer(CellData, &ParsedScriptEntry, nullptr, PPF_None) == nullptr)
|
|
{
|
|
UE_LOG(LogImportDialogueScriptCommandlet, Error, TEXT("Failed to parse the required column '%s' for row '%d' for culture '%s'."), *LocalizationKeysProperty->GetName(), RowIndex, *InCultureName);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
for (const FString& ContextLocalizationKey : ParsedScriptEntry.LocalizationKeys)
|
|
{
|
|
// Find the manifest entry so that we can find the corresponding archive entry
|
|
TSharedPtr<FManifestEntry> ContextManifestEntry = InLocTextHelper.FindSourceText(FDialogueConstants::DialogueNamespace, ContextLocalizationKey);
|
|
if (!ContextManifestEntry.IsValid())
|
|
{
|
|
UE_LOG(LogImportDialogueScriptCommandlet, Log, TEXT("No internationalization manifest entry was found for context '%s' in culture '%s'. This context will be skipped."), *ContextLocalizationKey, *InCultureName);
|
|
continue;
|
|
}
|
|
|
|
// Find the correct entry for our context
|
|
const FManifestContext* ContextManifestEntryContext = ContextManifestEntry->FindContextByKey(ContextLocalizationKey);
|
|
check(ContextManifestEntryContext); // This should never fail as we pass in the key to FindSourceText
|
|
|
|
// Get the text we would have exported
|
|
FLocItem ExportedSource;
|
|
FLocItem ExportedTranslation;
|
|
InLocTextHelper.GetExportText(InCultureName, FDialogueConstants::DialogueNamespace, ContextManifestEntryContext->Key, ContextManifestEntryContext->KeyMetadataObj, ELocTextExportSourceMethod::NativeText, ContextManifestEntry->Source, ExportedSource, ExportedTranslation);
|
|
|
|
// Attempt to import the new text (if required)
|
|
if (!ExportedTranslation.Text.Equals(ParsedScriptEntry.SpokenDialogue, ESearchCase::CaseSensitive))
|
|
{
|
|
if (InLocTextHelper.ImportTranslation(InCultureName, FDialogueConstants::DialogueNamespace, ContextManifestEntryContext->Key, ContextManifestEntryContext->KeyMetadataObj, ExportedSource, FLocItem(ParsedScriptEntry.SpokenDialogue), ContextManifestEntryContext->bIsOptional))
|
|
{
|
|
bHasUpdatedArchive = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Write out the updated archive file
|
|
if (bHasUpdatedArchive)
|
|
{
|
|
FText SaveError;
|
|
if (!InLocTextHelper.SaveArchive(InCultureName, &SaveError))
|
|
{
|
|
UE_LOG(LogImportDialogueScriptCommandlet, Error, TEXT("%s"), *SaveError.ToString());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|