void UInternationalizationExportCommandlet::PreserveExtractedCommentsForPersistence(FPortableObjectFormatDOM& PortableObject)
{
	// Preserve comments for later.
	for (auto EntriesIterator = PortableObject.GetEntriesIterator(); EntriesIterator; ++EntriesIterator)
	{
		const TSharedPtr< FPortableObjectEntry >& Entry = *EntriesIterator;

		// Preserve only non-procedurally generated extracted comments.
		const TArray<FString> CommentsToPreserve = Entry->ExtractedComments.FilterByPredicate([=](const FString& ExtractedComment) -> bool
		{
			return !ExtractedComment.StartsWith("Key:") && !ExtractedComment.StartsWith("SourceLocation:") && !ExtractedComment.StartsWith("InfoMetaData:");
		});

		if (CommentsToPreserve.Num())
		{
			POEntryToCommentMap.Add(FPortableObjectEntryIdentity{ Entry->MsgCtxt, Entry->MsgId, Entry->MsgIdPlural }, CommentsToPreserve);
		}
	}
	HasPreservedComments = true;
}
bool UInternationalizationExportCommandlet::LoadPOFile(const FString& POFilePath, FPortableObjectFormatDOM& OutPortableObject)
{
	if (!FPaths::FileExists(POFilePath))
	{
		UE_LOG(LogInternationalizationExportCommandlet, Warning, TEXT("Could not find file %s"), *POFilePath);
		return false;
	}

	FString POFileContents;
	if (!FFileHelper::LoadFileToString(POFileContents, *POFilePath))
	{
		UE_LOG(LogInternationalizationExportCommandlet, Error, TEXT("Failed to load file %s."), *POFilePath);
		return false;
	}

	if (!OutPortableObject.FromString(POFileContents))
	{
		UE_LOG(LogInternationalizationExportCommandlet, Error, TEXT("Failed to parse Portable Object file %s."), *POFilePath);
		return false;
	}

	return true;
}
bool UInternationalizationExportCommandlet::DoImport(const FString& SourcePath, const FString& DestinationPath, const FString& Filename)
{
	// Get manifest name.
	FString ManifestName;
	if( !GetStringFromConfig( *SectionName, TEXT("ManifestName"), ManifestName, ConfigPath ) )
	{
		UE_LOG( LogInternationalizationExportCommandlet, Error, TEXT("No manifest name specified.") );
		return false;
	}

	// Get archive name.
	FString ArchiveName;
	if( !( GetStringFromConfig(* SectionName, TEXT("ArchiveName"), ArchiveName, ConfigPath ) ) )
	{
		UE_LOG(LogInternationalizationExportCommandlet, Error, TEXT("No archive name specified."));
		return false;
	}

	// Get culture directory setting, default to true if not specified (used to allow picking of import directory with file open dialog from Translation Editor)
	bool bUseCultureDirectory = true;
	if (!(GetBoolFromConfig(*SectionName, TEXT("bUseCultureDirectory"), bUseCultureDirectory, ConfigPath)))
	{
		bUseCultureDirectory = true;
	}

	// Process the desired cultures
	for(int32 Culture = 0; Culture < CulturesToGenerate.Num(); Culture++)
	{
		// Load the Portable Object file if found
		const FString CultureName = CulturesToGenerate[Culture];
		FString POFilePath = "";
		if (bUseCultureDirectory)
		{
			POFilePath = SourcePath / CultureName / Filename;
		}
		else
		{
			POFilePath = SourcePath / Filename;
		}

		FPortableObjectFormatDOM PortableObject;
		const bool HasLoadedPOFile = LoadPOFile(POFilePath, PortableObject);
		if (!HasLoadedPOFile)
		{
			continue;
		}

		if (ShouldPersistComments)
		{
			PreserveExtractedCommentsForPersistence(PortableObject);
		}

		if (PortableObject.GetProjectName() != ManifestName.Replace(TEXT(".manifest"), TEXT("")))
		{
			UE_LOG(LogInternationalizationExportCommandlet, Log, TEXT("The project name (%s) in the file (%s) did not match the target manifest project (%s)."), *POFilePath, *PortableObject.GetProjectName(), *ManifestName.Replace(TEXT(".manifest"), TEXT("")));
		}

		const FString ManifestFileName = DestinationPath / ManifestName;

		TSharedPtr< FJsonObject > ManifestJsonObject = NULL;
		ManifestJsonObject = ReadJSONTextFile( ManifestFileName );

		FJsonInternationalizationManifestSerializer ManifestSerializer;
		TSharedRef< FInternationalizationManifest > InternationalizationManifest = MakeShareable( new FInternationalizationManifest );
		ManifestSerializer.DeserializeManifest( ManifestJsonObject.ToSharedRef(), InternationalizationManifest );

		if( !FPaths::FileExists(ManifestFileName) )
		{
			UE_LOG( LogInternationalizationExportCommandlet, Error, TEXT("Failed to find manifest %s."), *ManifestFileName);
			continue;
		}

		const FString DestinationCulturePath = DestinationPath / CultureName;
		const FString ArchiveFileName = DestinationCulturePath / ArchiveName;

		if( !FPaths::FileExists(ArchiveFileName) )
		{
			UE_LOG( LogInternationalizationExportCommandlet, Error, TEXT("Failed to find destination archive %s."), *ArchiveFileName);
			continue;
		}

		TSharedPtr< FJsonObject > ArchiveJsonObject = NULL;
		ArchiveJsonObject = ReadJSONTextFile( ArchiveFileName );

		FJsonInternationalizationArchiveSerializer ArchiveSerializer;
		TSharedRef< FInternationalizationArchive > InternationalizationArchive = MakeShareable( new FInternationalizationArchive );
		ArchiveSerializer.DeserializeArchive( ArchiveJsonObject.ToSharedRef(), InternationalizationArchive );

		bool bModifiedArchive = false;
		{
			for( auto EntryIter = PortableObject.GetEntriesIterator(); EntryIter; ++EntryIter )
			{
				auto POEntry = *EntryIter;
				if( POEntry->MsgId.IsEmpty() || POEntry->MsgStr.Num() == 0 || POEntry->MsgStr[0].Trim().IsEmpty() )
				{
					// We ignore the header entry or entries with no translation.
					continue;
				}

				// Some warning messages for data we don't process at the moment
				if( !POEntry->MsgIdPlural.IsEmpty() || POEntry->MsgStr.Num() > 1 )
				{
					UE_LOG( LogInternationalizationExportCommandlet, Error, TEXT("Portable Object entry has plural form we did not process.  File: %s  MsgCtxt: %s  MsgId: %s"), *POFilePath, *POEntry->MsgCtxt, *POEntry->MsgId );
				}

				FString Key;
				FString Namespace;
				ParsePOMsgCtxtForIdentity(POEntry->MsgCtxt, Namespace, Key);
				const FString& SourceText = ConditionPoStringForArchive(POEntry->MsgId);
				const FString& Translation = ConditionPoStringForArchive(POEntry->MsgStr[0]);

				TSharedPtr<FLocMetadataObject> KeyMetaDataObject;
				// Get key metadata from the manifest, using the namespace and key.
				if (!Key.IsEmpty())
				{
					// Find manifest entry by namespace
					for (auto ManifestEntryIterator = InternationalizationManifest->GetEntriesByContextIdIterator(); ManifestEntryIterator; ++ManifestEntryIterator)
					{
						const FString& ManifestEntryNamespace = ManifestEntryIterator->Key;
						const TSharedRef<FManifestEntry>& ManifestEntry = ManifestEntryIterator->Value;
						if (ManifestEntry->Namespace == Namespace)
						{
							FContext* const MatchingContext = ManifestEntry->Contexts.FindByPredicate([&](FContext& Context) -> bool
								{
									return Context.Key == Key;
								});
							if (MatchingContext)
							{
								KeyMetaDataObject = MatchingContext->KeyMetadataObj;
							}
						}
					}
				}

				//@TODO: Take into account optional entries and entries that differ by keymetadata.  Ex. Each optional entry needs a unique msgCtxt
				const TSharedPtr< FArchiveEntry > FoundEntry = InternationalizationArchive->FindEntryBySource( Namespace, SourceText, KeyMetaDataObject );
				if( !FoundEntry.IsValid() )
				{
					UE_LOG(LogInternationalizationExportCommandlet, Warning, TEXT("Could not find corresponding archive entry for PO entry.  File: %s  MsgCtxt: %s  MsgId: %s"), *POFilePath, *POEntry->MsgCtxt, *POEntry->MsgId );
					continue;
				}

				if( FoundEntry->Translation != Translation )
				{
					FoundEntry->Translation = Translation;
					bModifiedArchive = true;
				}
			}
		}

		if( bModifiedArchive )
		{
			TSharedRef<FJsonObject> FinalArchiveJsonObj = MakeShareable( new FJsonObject );
			ArchiveSerializer.SerializeArchive( InternationalizationArchive, FinalArchiveJsonObj );

			if( !WriteJSONToTextFile(FinalArchiveJsonObj, ArchiveFileName, SourceControlInfo ) )
			{
				UE_LOG( LogInternationalizationExportCommandlet, Error, TEXT("Failed to write archive to %s."), *ArchiveFileName );				
				return false;
			}
		}
	}

	return true;
}
/**
*	UInternationalizationExportCommandlet
*/
bool UInternationalizationExportCommandlet::DoExport( const FString& SourcePath, const FString& DestinationPath, const FString& Filename )
{
	// Get native culture.
	FString NativeCultureName;
	if( !GetStringFromConfig( *SectionName, TEXT("NativeCulture"), NativeCultureName, ConfigPath ) )
	{
		UE_LOG( LogInternationalizationExportCommandlet, Error, TEXT("No native culture specified.") );
		return false;
	}

	// Get manifest name.
	FString ManifestName;
	if( !GetStringFromConfig( *SectionName, TEXT("ManifestName"), ManifestName, ConfigPath ) )
	{
		UE_LOG( LogInternationalizationExportCommandlet, Error, TEXT("No manifest name specified.") );
		return false;
	}

	// Get archive name.
	FString ArchiveName;
	if( !( GetStringFromConfig(* SectionName, TEXT("ArchiveName"), ArchiveName, ConfigPath ) ) )
	{
		UE_LOG(LogInternationalizationExportCommandlet, Error, TEXT("No archive name specified."));
		return false;
	}

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

	bool ShouldAddSourceLocationsAsComments = true;
	GetBoolFromConfig(*SectionName, TEXT("ShouldAddSourceLocationsAsComments"), ShouldAddSourceLocationsAsComments, ConfigPath);

	TSharedRef< FInternationalizationManifest > InternationalizationManifest = MakeShareable( new FInternationalizationManifest );
	// Load the manifest info
	{
		FString ManifestFilePath = SourcePath / ManifestName;
		if( !FPaths::FileExists(ManifestFilePath) )
		{
			UE_LOG(LogInternationalizationExportCommandlet, Error, TEXT("Could not find manifest file %s."), *ManifestFilePath);
			return false;
		}

		TSharedPtr<FJsonObject> ManifestJsonObject = ReadJSONTextFile( ManifestFilePath );

		if( !ManifestJsonObject.IsValid() )
		{
			UE_LOG(LogInternationalizationExportCommandlet, Error, TEXT("Could not read manifest file %s."), *ManifestFilePath);
			return false;
		}

		FJsonInternationalizationManifestSerializer ManifestSerializer;
		ManifestSerializer.DeserializeManifest( ManifestJsonObject.ToSharedRef(), InternationalizationManifest );
	}

	TArray< TSharedPtr<FInternationalizationArchive> > NativeArchives;
	{
		const FString NativeCulturePath = SourcePath / *(NativeCultureName);
		TArray<FString> NativeArchiveFileNames;
		IFileManager::Get().FindFiles(NativeArchiveFileNames, *(NativeCulturePath / TEXT("*.archive")), true, false);

		for (const FString& NativeArchiveFileName : NativeArchiveFileNames)
		{
			// Read each archive file from the culture-named directory in the source path.
			FString ArchiveFilePath = NativeCulturePath / NativeArchiveFileName;
			ArchiveFilePath = FPaths::ConvertRelativePathToFull(ArchiveFilePath);
			TSharedRef<FInternationalizationArchive> InternationalizationArchive = MakeShareable(new FInternationalizationArchive);
			TSharedPtr< FJsonObject > ArchiveJsonObject = ReadJSONTextFile( ArchiveFilePath );
			FJsonInternationalizationArchiveSerializer ArchiveSerializer;
			ArchiveSerializer.DeserializeArchive( ArchiveJsonObject.ToSharedRef(), InternationalizationArchive );

			NativeArchives.Add(InternationalizationArchive);
		}
	}

	// Process the desired cultures
	for(int32 Culture = 0; Culture < CulturesToGenerate.Num(); Culture++)
	{
		// Load the archive
		const FString CultureName = CulturesToGenerate[Culture];
		const FString CulturePath = SourcePath / CultureName;
		FString ArchiveFileName = CulturePath / ArchiveName;
		TSharedPtr< FJsonObject > ArchiveJsonObject = NULL;

		if( FPaths::FileExists(ArchiveFileName) )
		{
			ArchiveJsonObject = ReadJSONTextFile( ArchiveFileName );

			FJsonInternationalizationArchiveSerializer ArchiveSerializer;
			TSharedRef< FInternationalizationArchive > InternationalizationArchive = MakeShareable( new FInternationalizationArchive );
			ArchiveSerializer.DeserializeArchive( ArchiveJsonObject.ToSharedRef(), InternationalizationArchive );

			{
				FPortableObjectFormatDOM NewPortableObject;

				FString LocLang;
				if( !NewPortableObject.SetLanguage( CultureName ) )
				{
					UE_LOG( LogInternationalizationExportCommandlet, Error, TEXT("Skipping export of loc language %s because it is not recognized."), *LocLang );
					continue;
				}

				NewPortableObject.SetProjectName( FPaths::GetBaseFilename( ManifestName ) );
				NewPortableObject.CreateNewHeader();

				{
					for(TManifestEntryBySourceTextContainer::TConstIterator ManifestIter = InternationalizationManifest->GetEntriesBySourceTextIterator(); ManifestIter; ++ManifestIter)
					{
						// Gather relevant info from manifest entry.
						const TSharedRef<FManifestEntry>& ManifestEntry = ManifestIter.Value();
						const FString& Namespace = ManifestEntry->Namespace;
						const FLocItem& Source = ManifestEntry->Source;

						// For each context, we may need to create a different or even multiple PO entries.
						for( auto ContextIter = ManifestEntry->Contexts.CreateConstIterator(); ContextIter; ++ContextIter )
						{
							const FContext& Context = *ContextIter;

							// Create the typical PO entry from the archive entry which matches the exact same namespace, source, and key metadata, if it exists.
							{
								const TSharedPtr<FArchiveEntry> ArchiveEntry = InternationalizationArchive->FindEntryBySource( Namespace, Source, Context.KeyMetadataObj );
								if( ArchiveEntry.IsValid() )
								{
									const FString ConditionedArchiveSource = ConditionArchiveStrForPo(ArchiveEntry->Source.Text);
									const FString ConditionedArchiveTranslation = ConditionArchiveStrForPo(ArchiveEntry->Translation.Text);

									TSharedRef<FPortableObjectEntry> PoEntry = MakeShareable( new FPortableObjectEntry );
									//@TODO: We support additional metadata entries that can be translated.  How do those fit in the PO file format?  Ex: isMature
									PoEntry->MsgId = ConditionedArchiveSource;
									PoEntry->MsgCtxt = ConditionIdentityForPOMsgCtxt(Namespace, Context.Key, Context.KeyMetadataObj);
									PoEntry->MsgStr.Add( ConditionedArchiveTranslation );

									const FString PORefString = ConvertSrcLocationToPORef( Context.SourceLocation );
									PoEntry->AddReference(PORefString); // Source location.

									PoEntry->AddExtractedComment( GetConditionedKeyForExtractedComment(Context.Key) ); // "Notes from Programmer" in the form of the Key.

									if (ShouldAddSourceLocationsAsComments)
									{
										PoEntry->AddExtractedComment(GetConditionedReferenceForExtractedComment(PORefString)); // "Notes from Programmer" in the form of the Source Location, since this comes in handy too and OneSky doesn't properly show references, only comments.
									}

									TArray<FString> InfoMetaDataStrings;
									if (Context.InfoMetadataObj.IsValid())
									{
										for (auto InfoMetaDataPair : Context.InfoMetadataObj->Values)
										{
											const FString KeyName = InfoMetaDataPair.Key;
											const TSharedPtr<FLocMetadataValue> Value = InfoMetaDataPair.Value;
											InfoMetaDataStrings.Add(GetConditionedInfoMetaDataForExtractedComment(KeyName, Value->AsString()));
										}
									}
									if (InfoMetaDataStrings.Num())
									{
										PoEntry->AddExtractedComments(InfoMetaDataStrings);
									}

									NewPortableObject.AddEntry( PoEntry );
								}
							}

							// If we're exporting for something other than the native culture, we'll need to create PO entries for archive entries based on the native archive's translation.
							if (CultureName != NativeCultureName)
							{
								TSharedPtr<FArchiveEntry> NativeArchiveEntry;
								// Find the native archive entry which matches the exact same namespace, source, and key metadata, if it exists.
								for (const auto& NativeArchive : NativeArchives)
								{
									const TSharedPtr<FArchiveEntry> PotentialNativeArchiveEntry = NativeArchive->FindEntryBySource( Namespace, Source, Context.KeyMetadataObj );
									if (PotentialNativeArchiveEntry.IsValid())
									{
										NativeArchiveEntry = PotentialNativeArchiveEntry;
										break;
									}
								}

								if (NativeArchiveEntry.IsValid())
								{
									// Only need to create this PO entry if the native archive entry's translation differs from its source, in which case we need to find the our translation of the native translation.
									if (!NativeArchiveEntry->Source.IsExactMatch(NativeArchiveEntry->Translation))
									{
										const TSharedPtr<FArchiveEntry> ArchiveEntry = InternationalizationArchive->FindEntryBySource( Namespace, NativeArchiveEntry->Translation, NativeArchiveEntry->KeyMetadataObj );
										if (ArchiveEntry.IsValid())
										{
											const FString ConditionedArchiveSource = ConditionArchiveStrForPo(ArchiveEntry->Source.Text);
											const FString ConditionedArchiveTranslation = ConditionArchiveStrForPo(ArchiveEntry->Translation.Text);

											TSharedRef<FPortableObjectEntry> PoEntry = MakeShareable( new FPortableObjectEntry );
											//@TODO: We support additional metadata entries that can be translated.  How do those fit in the PO file format?  Ex: isMature
											PoEntry->MsgId = ConditionedArchiveSource;
											PoEntry->MsgCtxt = ConditionIdentityForPOMsgCtxt(Namespace, Context.Key, Context.KeyMetadataObj);
											PoEntry->MsgStr.Add( ConditionedArchiveTranslation );

											const FString PORefString = ConvertSrcLocationToPORef( Context.SourceLocation );
											PoEntry->AddReference( PORefString ); // Source location.

											PoEntry->AddExtractedComment( FString::Printf(TEXT("Key:\t%s"), *Context.Key) ); // "Notes from Programmer" in the form of the Key.
											PoEntry->AddExtractedComment( FString::Printf(TEXT("SourceLocation:\t%s"), *PORefString) ); // "Notes from Programmer" in the form of the Source Location, since this comes in handy too and OneSky doesn't properly show references, only comments.
											TArray<FString> InfoMetaDataStrings;
											if (Context.InfoMetadataObj.IsValid())
											{
												for (auto InfoMetaDataPair : Context.InfoMetadataObj->Values)
												{
													const FString KeyName = InfoMetaDataPair.Key;
													const TSharedPtr<FLocMetadataValue> Value = InfoMetaDataPair.Value;
													InfoMetaDataStrings.Add(FString::Printf(TEXT("InfoMetaData:\t\"%s\" : \"%s\""), *KeyName, *Value->AsString()));
												}
											}
											if (InfoMetaDataStrings.Num())
											{
												PoEntry->AddExtractedComments(InfoMetaDataStrings);
											}

											NewPortableObject.AddEntry( PoEntry );
										}
									}
								}
							}
						}
					}
				}

				// Write out the Portable Object to .po file.
				{
					FString OutputFileName;
					if (bUseCultureDirectory)
					{
						OutputFileName = DestinationPath / CultureName / Filename;
					}
					else
					{
						OutputFileName = DestinationPath / Filename;
					}

					// Persist comments if requested.
					if (ShouldPersistComments)
					{
						// Preserve comments from the specified file now, if they haven't already been.
						if (!HasPreservedComments)
						{
							FPortableObjectFormatDOM ExistingPortableObject;
							const bool HasLoadedPOFile = LoadPOFile(OutputFileName, ExistingPortableObject);
							if (!HasLoadedPOFile)
							{
								return false;
							}

							PreserveExtractedCommentsForPersistence(ExistingPortableObject);
						}

						// Persist the comments into the new portable object we're going to be saving.
						for (const auto& Pair : POEntryToCommentMap)
						{
							const TSharedPtr<FPortableObjectEntry> FoundEntry = NewPortableObject.FindEntry(Pair.Key.MsgId, Pair.Key.MsgIdPlural, Pair.Key.MsgCtxt);
							if (FoundEntry.IsValid())
							{
								FoundEntry->AddExtractedComments(Pair.Value);
							}
						}
					}

					NewPortableObject.SortEntries();

					if( SourceControlInfo.IsValid() )
					{
						FText SCCErrorText;
						if (!SourceControlInfo->CheckOutFile(OutputFileName, SCCErrorText))
						{
							UE_LOG(LogInternationalizationExportCommandlet, Error, TEXT("Check out of file %s failed: %s"), *OutputFileName, *SCCErrorText.ToString());
							return false;
						}
					}

					//@TODO We force UTF8 at the moment but we want this to be based on the format found in the header info.
					const FString OutputString = NewPortableObject.ToString();
					if (!FFileHelper::SaveStringToFile(OutputString, *OutputFileName, FFileHelper::EEncodingOptions::ForceUTF8))
					{
						UE_LOG( LogInternationalizationExportCommandlet, Error, TEXT("Could not write file %s"), *OutputFileName );
						return false;
					}
				}
			}
		}
	}
	return true;
}
bool UInternationalizationExportCommandlet::DoImport(const FString& SourcePath, const FString& DestinationPath, const FString& Filename)
{
	// Get manifest name.
	FString ManifestName;
	if( !GetStringFromConfig( *SectionName, TEXT("ManifestName"), ManifestName, ConfigPath ) )
	{
		UE_LOG( LogInternationalizationExportCommandlet, Error, TEXT("No manifest name specified.") );
		return false;
	}

	// Get archive name.
	FString ArchiveName;
	if( !( GetStringFromConfig(* SectionName, TEXT("ArchiveName"), ArchiveName, ConfigPath ) ) )
	{
		UE_LOG(LogInternationalizationExportCommandlet, Error, TEXT("No archive name specified."));
		return false;
	}

	// Get culture directory setting, default to true if not specified (used to allow picking of import directory with file open dialog from Translation Editor)
	bool bUseCultureDirectory = true;
	if (!(GetBoolFromConfig(*SectionName, TEXT("bUseCultureDirectory"), bUseCultureDirectory, ConfigPath)))
	{
		bUseCultureDirectory = true;
	}

	// Process the desired cultures
	for(int32 Culture = 0; Culture < CulturesToGenerate.Num(); Culture++)
	{
		// Load the Portable Object file if found
		const FString CultureName = CulturesToGenerate[Culture];
		FString POFilePath = "";
		if (bUseCultureDirectory)
		{
			POFilePath = SourcePath / CultureName / Filename;
		}
		else
		{
			POFilePath = SourcePath / Filename;
		}

		if( !FPaths::FileExists(POFilePath) )
		{
			UE_LOG( LogInternationalizationExportCommandlet, Warning, TEXT("Could not find file %s"), *POFilePath );
			continue;
		}

		FString POFileContents;
		if ( !FFileHelper::LoadFileToString( POFileContents, *POFilePath ) )
		{
			UE_LOG( LogInternationalizationExportCommandlet, Error, TEXT("Failed to load file %s."), *POFilePath);
			continue;
		}

		FPortableObjectFormatDOM PortableObject;
		if( !PortableObject.FromString( POFileContents ) )
		{
			UE_LOG( LogInternationalizationExportCommandlet, Error, TEXT("Failed to parse Portable Object file %s."), *POFilePath);
			continue;
		}

		if( PortableObject.GetProjectName() != ManifestName.Replace(TEXT(".manifest"), TEXT("")) )
		{
			UE_LOG( LogInternationalizationExportCommandlet, Warning, TEXT("The project name (%s) in the file (%s) did not match the target manifest project (%s)."), *POFilePath, *PortableObject.GetProjectName(), *ManifestName.Replace(TEXT(".manifest"), TEXT("")));
		}


		const FString DestinationCulturePath = DestinationPath / CultureName;
		FString ArchiveFileName = DestinationCulturePath / ArchiveName;
		
		if( !FPaths::FileExists(ArchiveFileName) )
		{
			UE_LOG( LogInternationalizationExportCommandlet, Error, TEXT("Failed to find destination archive %s."), *ArchiveFileName);
			continue;
		}

		TSharedPtr< FJsonObject > ArchiveJsonObject = NULL;
		ArchiveJsonObject = ReadJSONTextFile( ArchiveFileName );

		FJsonInternationalizationArchiveSerializer ArchiveSerializer;
		TSharedRef< FInternationalizationArchive > InternationalizationArchive = MakeShareable( new FInternationalizationArchive );
		ArchiveSerializer.DeserializeArchive( ArchiveJsonObject.ToSharedRef(), InternationalizationArchive );

		bool bModifiedArchive = false;
		{
			for( auto EntryIter = PortableObject.GetEntriesIterator(); EntryIter; ++EntryIter )
			{
				auto POEntry = *EntryIter;
				if( POEntry->MsgId.IsEmpty() || POEntry->MsgStr.Num() == 0 || POEntry->MsgStr[0].Trim().IsEmpty() )
				{
					// We ignore the header entry or entries with no translation.
					continue;
				}

				// Some warning messages for data we don't process at the moment
				if( !POEntry->MsgIdPlural.IsEmpty() || POEntry->MsgStr.Num() > 1 )
				{
					UE_LOG( LogInternationalizationExportCommandlet, Error, TEXT("Portable Object entry has plural form we did not process.  File: %s  MsgCtxt: %s  MsgId: %s"), *POFilePath, *POEntry->MsgCtxt, *POEntry->MsgId );
				}
				
				const FString& Namespace = POEntry->MsgCtxt;
				const FString& SourceText = ConditionPoStringForArchive(POEntry->MsgId);
				const FString& Translation = ConditionPoStringForArchive(POEntry->MsgStr[0]);

				//@TODO: Take into account optional entries and entries that differ by keymetadata.  Ex. Each optional entry needs a unique msgCtxt
				TSharedPtr< FArchiveEntry > FoundEntry = InternationalizationArchive->FindEntryBySource( Namespace, SourceText, NULL );
				if( !FoundEntry.IsValid() )
				{
					UE_LOG(LogInternationalizationExportCommandlet, Warning, TEXT("Could not find corresponding archive entry for PO entry.  File: %s  MsgCtxt: %s  MsgId: %s"), *POFilePath, *POEntry->MsgCtxt, *POEntry->MsgId );
					continue;
				}
				
				if( FoundEntry->Translation != Translation )
				{
					FoundEntry->Translation = Translation;
					bModifiedArchive = true;
				}
			}
		}

		if( bModifiedArchive )
		{
			TSharedRef<FJsonObject> FinalArchiveJsonObj = MakeShareable( new FJsonObject );
			ArchiveSerializer.SerializeArchive( InternationalizationArchive, FinalArchiveJsonObj );

			if( !WriteJSONToTextFile(FinalArchiveJsonObj, ArchiveFileName, SourceControlInfo ) )
			{
				UE_LOG( LogInternationalizationExportCommandlet, Error, TEXT("Failed to write archive to %s."), *ArchiveFileName );				
				return false;
			}
		}
	}

	return true;
}
bool UInternationalizationExportCommandlet::DoExport( const FString& SourcePath, const FString& DestinationPath, const FString& Filename )
{
	// Get native culture.
	FString NativeCultureName;
	if( !GetStringFromConfig( *SectionName, TEXT("NativeCulture"), NativeCultureName, ConfigPath ) )
	{
		UE_LOG( LogInternationalizationExportCommandlet, Error, TEXT("No native culture specified.") );
		return false;
	}

	// Get manifest name.
	FString ManifestName;
	if( !GetStringFromConfig( *SectionName, TEXT("ManifestName"), ManifestName, ConfigPath ) )
	{
		UE_LOG( LogInternationalizationExportCommandlet, Error, TEXT("No manifest name specified.") );
		return false;
	}

	// Get archive name.
	FString ArchiveName;
	if( !( GetStringFromConfig(* SectionName, TEXT("ArchiveName"), ArchiveName, ConfigPath ) ) )
	{
		UE_LOG(LogInternationalizationExportCommandlet, Error, TEXT("No archive name specified."));
		return false;
	}

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


	TSharedRef< FInternationalizationManifest > InternationalizationManifest = MakeShareable( new FInternationalizationManifest );
	// Load the manifest info
	{
		FString ManifestFilePath = SourcePath / ManifestName;
		if( !FPaths::FileExists(ManifestFilePath) )
		{
			UE_LOG(LogInternationalizationExportCommandlet, Error, TEXT("Could not find manifest file %s."), *ManifestFilePath);
			return false;
		}

		TSharedPtr<FJsonObject> ManifestJsonObject = ReadJSONTextFile( ManifestFilePath );

		if( !ManifestJsonObject.IsValid() )
		{
			UE_LOG(LogInternationalizationExportCommandlet, Error, TEXT("Could not read manifest file %s."), *ManifestFilePath);
			return false;
		}

		FJsonInternationalizationManifestSerializer ManifestSerializer;
		ManifestSerializer.DeserializeManifest( ManifestJsonObject.ToSharedRef(), InternationalizationManifest );
	}

	TArray< TSharedPtr<FInternationalizationArchive> > NativeArchives;
	{
		const FString NativeCulturePath = DestinationPath / *(NativeCultureName);
		TArray<FString> NativeArchiveFileNames;
		IFileManager::Get().FindFiles(NativeArchiveFileNames, *(NativeCulturePath / TEXT("*.archive")), true, false);

		for (const FString& NativeArchiveFileName : NativeArchiveFileNames)
		{
			// Read each archive file from the culture-named directory in the source path.
			FString ArchiveFilePath = NativeCulturePath / NativeArchiveFileName;
			ArchiveFilePath = FPaths::ConvertRelativePathToFull(ArchiveFilePath);
			TSharedRef<FInternationalizationArchive> InternationalizationArchive = MakeShareable(new FInternationalizationArchive);
			TSharedPtr< FJsonObject > ArchiveJsonObject = ReadJSONTextFile( ArchiveFilePath );
			FJsonInternationalizationArchiveSerializer ArchiveSerializer;
			ArchiveSerializer.DeserializeArchive( ArchiveJsonObject.ToSharedRef(), InternationalizationArchive );

			NativeArchives.Add(InternationalizationArchive);
		}
	}

	// Process the desired cultures
	for(int32 Culture = 0; Culture < CulturesToGenerate.Num(); Culture++)
	{
		// Load the archive
		const FString CultureName = CulturesToGenerate[Culture];
		const FString CulturePath = SourcePath / CultureName;
		FString ArchiveFileName = CulturePath / ArchiveName;
		TSharedPtr< FJsonObject > ArchiveJsonObject = NULL;

		if( FPaths::FileExists(ArchiveFileName) )
		{
			ArchiveJsonObject = ReadJSONTextFile( ArchiveFileName );

			FJsonInternationalizationArchiveSerializer ArchiveSerializer;
			TSharedRef< FInternationalizationArchive > InternationalizationArchive = MakeShareable( new FInternationalizationArchive );
			ArchiveSerializer.DeserializeArchive( ArchiveJsonObject.ToSharedRef(), InternationalizationArchive );

			{
				FPortableObjectFormatDOM PortableObj;

				FString LocLang;
				if( !PortableObj.SetLanguage( CultureName ) )
				{
					UE_LOG( LogInternationalizationExportCommandlet, Error, TEXT("Skipping export of loc language %s because it is not recognized."), *LocLang );
					continue;
				}

				PortableObj.SetProjectName( FPaths::GetBaseFilename( ManifestName ) );
				PortableObj.CreateNewHeader();

				{
					for(TManifestEntryBySourceTextContainer::TConstIterator ManifestIter = InternationalizationManifest->GetEntriesBySourceTextIterator(); ManifestIter; ++ManifestIter)
					{
						// Gather relevant info from manifest entry.
						const TSharedRef<FManifestEntry>& ManifestEntry = ManifestIter.Value();
						const FString& Namespace = ManifestEntry->Namespace;
						const FLocItem& Source = ManifestEntry->Source;

						for( auto ContextIter = ManifestEntry->Contexts.CreateConstIterator(); ContextIter; ++ContextIter )
						{
							{
								const TSharedPtr<FArchiveEntry> ArchiveEntry = InternationalizationArchive->FindEntryBySource( Namespace, Source, ContextIter->KeyMetadataObj );
							if( ArchiveEntry.IsValid() )
							{
								const FString ConditionedArchiveSource = ConditionArchiveStrForPo(ArchiveEntry->Source.Text);
								const FString ConditionedArchiveTranslation = ConditionArchiveStrForPo(ArchiveEntry->Translation.Text);

								TSharedRef<FPortableObjectEntry> PoEntry = MakeShareable( new FPortableObjectEntry );
								//@TODO: We support additional metadata entries that can be translated.  How do those fit in the PO file format?  Ex: isMature
								PoEntry->MsgId = ConditionedArchiveSource;
								//@TODO: Take into account optional entries and entries that differ by keymetadata.  Ex. Each optional entry needs a unique msgCtxt
								PoEntry->MsgCtxt = Namespace;
								PoEntry->MsgStr.Add( ConditionedArchiveTranslation );

								FString PORefString = ConvertSrcLocationToPORef( ContextIter->SourceLocation );
								PoEntry->AddReference( PORefString ); // Source location.
								PoEntry->AddExtractedComment( ContextIter->Key ); // "Notes from Programmer" in the form of the Key.
								PoEntry->AddExtractedComment( PORefString ); // "Notes from Programmer" in the form of the Source Location, since this comes in handy too and OneSky doesn't properly show references, only comments.
								PortableObj.AddEntry( PoEntry );
							}
						}

							if (CultureName != NativeCultureName)
							{
								TSharedPtr<FArchiveEntry> NativeArchiveEntry;
								for (const auto& NativeArchive : NativeArchives)
								{
									const TSharedPtr<FArchiveEntry> PotentialNativeArchiveEntry = NativeArchive->FindEntryBySource( Namespace, Source, ContextIter->KeyMetadataObj );
									if (PotentialNativeArchiveEntry.IsValid())
									{
										NativeArchiveEntry = PotentialNativeArchiveEntry;
										break;
									}
								}

								if (NativeArchiveEntry.IsValid())
								{
									if (!NativeArchiveEntry->Source.IsExactMatch(NativeArchiveEntry->Translation))
									{
										const TSharedPtr<FArchiveEntry> ArchiveEntry = InternationalizationArchive->FindEntryBySource( Namespace, NativeArchiveEntry->Translation, NativeArchiveEntry->KeyMetadataObj );

										const FString ConditionedArchiveSource = ConditionArchiveStrForPo(ArchiveEntry->Source.Text);
										const FString ConditionedArchiveTranslation = ConditionArchiveStrForPo(ArchiveEntry->Translation.Text);

										TSharedRef<FPortableObjectEntry> PoEntry = MakeShareable( new FPortableObjectEntry );
										//@TODO: We support additional metadata entries that can be translated.  How do those fit in the PO file format?  Ex: isMature
										PoEntry->MsgId = ConditionedArchiveSource;
										//@TODO: Take into account optional entries and entries that differ by keymetadata.  Ex. Each optional entry needs a unique msgCtxt
										PoEntry->MsgCtxt = Namespace;
										PoEntry->MsgStr.Add( ConditionedArchiveTranslation );

										FString PORefString = ConvertSrcLocationToPORef( ContextIter->SourceLocation );
										PoEntry->AddReference( PORefString ); // Source location.
										PoEntry->AddExtractedComment( ContextIter->Key ); // "Notes from Programmer" in the form of the Key.
										PoEntry->AddExtractedComment( PORefString ); // "Notes from Programmer" in the form of the Source Location, since this comes in handy too and OneSky doesn't properly show references, only comments.
										PortableObj.AddEntry( PoEntry );
									}
								}
					}
				}
					}
				}

				// Write out the Portable Object to .po file.
				{
					PortableObj.SortEntries();
					FString OutputString = PortableObj.ToString();
					FString OutputFileName = "";
					if (bUseCultureDirectory)
					{
						OutputFileName = DestinationPath / CultureName / Filename;
					}
					else
					{
						OutputFileName = DestinationPath / Filename;
					}

					if( SourceControlInfo.IsValid() )
					{
						FText SCCErrorText;
						if (!SourceControlInfo->CheckOutFile(OutputFileName, SCCErrorText))
						{
							UE_LOG(LogInternationalizationExportCommandlet, Error, TEXT("Check out of file %s failed: %s"), *OutputFileName, *SCCErrorText.ToString());
							return false;
						}
					}

					//@TODO We force UTF8 at the moment but we want this to be based on the format found in the header info.
					if( !FFileHelper::SaveStringToFile(OutputString, *OutputFileName, FFileHelper::EEncodingOptions::ForceUTF8) )
					{
						UE_LOG( LogInternationalizationExportCommandlet, Error, TEXT("Could not write file %s"), *OutputFileName );
						return false;
					}
				}
			}
		}
	}
	return true;
}