[UE4] FormatText 引数修飾子の拡張

UE4 でユーザに表示する文字列を扱うときは FText 型を使います
ローカライズ作業を行う上でも FText 前提で作られているので FString で実装を進めていると痛い目にあいます（経験談）

# FormatText 関数について
[テキスト ローカライゼーション](https://docs.unrealengine.com/ja/ProductionPipelines/Localization/Formatting/index.html)
>テキスト フォーマットは、実際のテキストを挿入してフォーマット プレース ホルダを置き換えるローカライズ可能なフォーマット パターンを使用してテキストを組み合わせることで、ローカライズを容易にします。

- "You have {CurrentHealth} health left."
- "残り体力 {CurrentHealth}"

こういった翻訳テキストデータにプレースホルダーを指定して、ランタイムでテキストを置換するための機能です

>引数修飾子を使用すると、フォーマット化された文字列に引数が追加される前に、引数を前処理できます。引数修飾子は拡張可能です。

引数修飾子に応じて異なるテキストを選択でき、処理の内容を自由に拡張できるようになっています

- 複数指定（There "is" "cat", There "are" "cats"）
- 性別指定（〇〇"くん", 〇〇"ちゃん"）
- ハングル文字助詞指定（分かんない）

とはいえ公式から用意されているのは上記3パターンしかないので、もっと汎用的に使えるように拡張してみよう、というのが今回の趣旨です

# TextFormatArgumentModifier クラスの拡張
引数修飾子を拡張するためには、ITextFormatArgumentModifier を継承したクラスを C++ で実装し TextFormatter に登録する必要があります

```C++
void FMyGameModule::StartupModule()
{
	FTextFormatter::Get().RegisterTextArgumentModifier( FTextFormatString::MakeReference( TEXT( "index" ) ),
		[]( const FTextFormatString& InArgsString, const FTextFormatPatternDefinitionConstRef& InPatternDef ) {
			return FTextFormatArgumentModifier_IndexForm::Create( InArgsString, InPatternDef );
		} );
	FTextFormatter::Get().RegisterTextArgumentModifier( FTextFormatString::MakeReference( TEXT( "enum" ) ),
		[]( const FTextFormatString& InArgsString, const FTextFormatPatternDefinitionConstRef& InPatternDef ) {
			return FTextFormatArgumentModifier_EnumForm::Create( InArgsString, InPatternDef );
		} );
}

void FMyGameModule::ShutdownModule()
{
	FTextFormatter::Get().UnregisterTextArgumentModifier( FTextFormatString::MakeReference( TEXT( "index" ) ) );
	FTextFormatter::Get().UnregisterTextArgumentModifier( FTextFormatString::MakeReference( TEXT( "enum" ) ) );
}
```

今回は 2 通りの実装を用意しました

## 配列の N 番目を指定する
> {Value}|index(N1,N2,N3...)

![0e57bf88df6f3b7ef0a51cf961c23e22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/9c26a601-ec99-f939-fb21-5013c7fc164a.png)
![910bc5ec805400fd226418a7b585189a.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/f7f0ff16-2d7c-2b0c-8f2c-e24d6ea5588e.png)
配列指定でフォームを選択します
Int 以外の変数を渡した場合や、配列の存在しないインデックスを渡した場合は何も表示されません（実装を書けばエラーとすることもできる）

<details><summary>実装コード</summary><div>

```C++:TextFormatArgumentModifier_IndexForm.h
#pragma once

#include "Internationalization/ITextFormatArgumentModifier.h"

class FTextFormatArgumentModifier_IndexForm : public ITextFormatArgumentModifier
{
public:
	static TSharedPtr<ITextFormatArgumentModifier> Create(
		const FTextFormatString& InArgsString, const FTextFormatPatternDefinitionConstRef& InPatternDef );

	virtual bool Validate( const FCultureRef& InCulture, TArray<FString>& OutValidationErrors ) const override;
	virtual void Evaluate(
		const FFormatArgumentValue& InValue, const FPrivateTextFormatArguments& InFormatArgs, FString& OutResult ) const override;
	virtual void GetFormatArgumentNames( TArray<FString>& OutArgumentNames ) const override;
	virtual void EstimateLength( int32& OutLength, bool& OutUsesFormatArgs ) const override;

private:
	FTextFormatArgumentModifier_IndexForm(
		TArray<FTextFormat>&& InArgForms, const int32 InLongestIndexFormStringLen, const bool InDoIndexFormsUseFormatArgs );

	int32 LongestIndexFormStringLen;
	bool bDoIndexFormsUseFormatArgs;
	TArray<FTextFormat> ArgForms;
};
```
```C++:TextFormatArgumentModifier_IndexForm.cpp
#include "TextFormatArgumentModifier_IndexForm.h"

#include "Internationalization/TextFormatter.h"

TSharedPtr<ITextFormatArgumentModifier> FTextFormatArgumentModifier_IndexForm::Create(
	const FTextFormatString& InArgsString, const FTextFormatPatternDefinitionConstRef& InPatternDef )
{
	TArray<FTextFormatString> ArgValues;
	if ( ParseValueArgs( InArgsString, ArgValues ) && ArgValues.Num() != 0 )
	{
		TArray<FTextFormat> ArgForms;
		int32 LongestIndexFormStringLen = 0;
		bool bDoIndexFormsUseFormatArgs = false;
		for ( const FTextFormatString& ArgValue : ArgValues )
		{
			FTextFormat& ArgForm = ArgForms.Emplace_GetRef(
				FTextFormat::FromString( FString( ArgValue.StringLen, ArgValue.StringPtr ), InPatternDef ) );
			LongestIndexFormStringLen = FMath::Max( LongestIndexFormStringLen, ArgValue.StringLen );
			bDoIndexFormsUseFormatArgs |= ArgForm.GetExpressionType() == FTextFormat::EExpressionType::Complex;
		}

		return MakeShareable( new FTextFormatArgumentModifier_IndexForm(
			MoveTemp( ArgForms ), LongestIndexFormStringLen, bDoIndexFormsUseFormatArgs ) );
	}
	return nullptr;
}

bool FTextFormatArgumentModifier_IndexForm::Validate( const FCultureRef& InCulture, TArray<FString>& OutValidationErrors ) const
{
	bool bIsValid = true;
	for ( const FTextFormat& ArgForm : ArgForms )
	{
		bIsValid &= ArgForm.ValidatePattern( InCulture, OutValidationErrors );
	}
	return bIsValid;
}

void FTextFormatArgumentModifier_IndexForm::Evaluate(
	const FFormatArgumentValue& InValue, const FPrivateTextFormatArguments& InFormatArgs, FString& OutResult ) const
{
	if ( InValue.GetType() == EFormatArgumentType::Int )
	{
		int64 IntValue = InValue.GetIntValue();
		if ( ArgForms.IsValidIndex( IntValue ) )
		{
			OutResult += FTextFormatter::Format( ArgForms[IntValue], InFormatArgs );
		}
	}
}

void FTextFormatArgumentModifier_IndexForm::GetFormatArgumentNames( TArray<FString>& OutArgumentNames ) const
{
	for ( const FTextFormat& ArgForm : ArgForms )
	{
		ArgForm.GetFormatArgumentNames( OutArgumentNames );
	}
}

void FTextFormatArgumentModifier_IndexForm::EstimateLength( int32& OutLength, bool& OutUsesFormatArgs ) const
{
	OutLength = LongestIndexFormStringLen;
	OutUsesFormatArgs = bDoIndexFormsUseFormatArgs;
}

FTextFormatArgumentModifier_IndexForm::FTextFormatArgumentModifier_IndexForm(
	TArray<FTextFormat>&& InArgForms, const int32 InLongestIndexFormStringLen, const bool InDoIndexFormsUseFormatArgs )
	: LongestIndexFormStringLen( InLongestIndexFormStringLen )
	, bDoIndexFormsUseFormatArgs( InDoIndexFormsUseFormatArgs )
	, ArgForms( MoveTemp( InArgForms ) )
{
}
```
</div></details>

## キーペアで指定する

> {Value}|enum(X1=Y1,X2=Y2,...)

![3e2134444e07090b22a733d505d4ea7e.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/c6b034ef-b377-68d4-359d-ab0baaaa6088.png)
![0e5422b86a2a9895d5106bb268525051.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/47cd7647-c5b7-3f17-aaed-0b5a04b38945.png)
キーペアを指定しフォームを選択します
キー指定が存在しなかった場合に "_" の値を使うオプションがあります
（Enum 値は FormatText 関数に直接は接続できないので、Int 変換を挟む必要があります）

<details><summary>実装コード</summary><div>

```C++:TextFormatArgumentModifier_EnumForm.h
#pragma once

#include "Internationalization/ITextFormatArgumentModifier.h"

class FTextFormatArgumentModifier_EnumForm : public ITextFormatArgumentModifier
{
public:
	static TSharedPtr<ITextFormatArgumentModifier> Create(
		const FTextFormatString& InArgsString, const FTextFormatPatternDefinitionConstRef& InPatternDef );

	virtual bool Validate( const FCultureRef& InCulture, TArray<FString>& OutValidationErrors ) const override;
	virtual void Evaluate(
		const FFormatArgumentValue& InValue, const FPrivateTextFormatArguments& InFormatArgs, FString& OutResult ) const override;
	virtual void GetFormatArgumentNames( TArray<FString>& OutArgumentNames ) const override;
	virtual void EstimateLength( int32& OutLength, bool& OutUsesFormatArgs ) const override;

private:
	FTextFormatArgumentModifier_EnumForm(
		TMap<FTextFormatString, FTextFormat>&& InArgForms, const int32 InLongestFormStringLen, const bool InDoFormsUseFormatArgs );

	int32 LongestFormStringLen;
	bool bDoFormsUseFormatArgs;
	TMap<FTextFormatString, FTextFormat> ArgForms;
};
```
```C++:TextFormatArgumentModifier_EnumForm.cpp
#include "TextFormatArgumentModifier_EnumForm.h"

#include "Internationalization/TextFormatter.h"

TSharedPtr<ITextFormatArgumentModifier> FTextFormatArgumentModifier_EnumForm::Create(
	const FTextFormatString& InArgsString, const FTextFormatPatternDefinitionConstRef& InPatternDef )
{
	TMap<FTextFormatString, FTextFormatString> ArgKeyValues;
	if ( ParseKeyValueArgs( InArgsString, ArgKeyValues ) && ArgKeyValues.Num() != 0 )
	{
		TMap<FTextFormatString, FTextFormat> ArgForms;
		int32 LongestFormStringLen = 0;
		bool bDoFormsUseFormatArgs = false;
		for ( const TPair<FTextFormatString, FTextFormatString>& Pair : ArgKeyValues )
		{
			FTextFormat& ArgForm = ArgForms.Emplace(
				Pair.Key, FTextFormat::FromString( FString( Pair.Value.StringLen, Pair.Value.StringPtr ), InPatternDef ) );
			LongestFormStringLen = FMath::Max( LongestFormStringLen, Pair.Value.StringLen );
			bDoFormsUseFormatArgs |= ArgForm.GetExpressionType() == FTextFormat::EExpressionType::Complex;
		}

		return MakeShareable(
			new FTextFormatArgumentModifier_EnumForm( MoveTemp( ArgForms ), LongestFormStringLen, bDoFormsUseFormatArgs ) );
	}
	return nullptr;
}

bool FTextFormatArgumentModifier_EnumForm::Validate( const FCultureRef& InCulture, TArray<FString>& OutValidationErrors ) const
{
	bool bIsValid = true;
	for ( const TPair<FTextFormatString, FTextFormat>& Pair : ArgForms )
	{
		bIsValid &= Pair.Value.ValidatePattern( InCulture, OutValidationErrors );
	}
	return bIsValid;
}

void FTextFormatArgumentModifier_EnumForm::Evaluate(
	const FFormatArgumentValue& InValue, const FPrivateTextFormatArguments& InFormatArgs, FString& OutResult ) const
{
	if ( InValue.GetType() == EFormatArgumentType::Int )
	{
		if ( const FTextFormat* FoundForm = ArgForms.Find( FString::FromInt( InValue.GetIntValue() ) ) )
		{
			OutResult += FTextFormatter::Format( *FoundForm, InFormatArgs );
		}
		else if ( const FTextFormat* ExtraForm = ArgForms.Find( FTextFormatString::MakeReference( TEXT( "_" ) ) ) )
		{
			OutResult += FTextFormatter::Format( *ExtraForm, InFormatArgs );
		}
	}
}

void FTextFormatArgumentModifier_EnumForm::GetFormatArgumentNames( TArray<FString>& OutArgumentNames ) const
{
	for ( const TPair<FTextFormatString, FTextFormat>& Pair : ArgForms )
	{
		Pair.Value.GetFormatArgumentNames( OutArgumentNames );
	}
}

void FTextFormatArgumentModifier_EnumForm::EstimateLength( int32& OutLength, bool& OutUsesFormatArgs ) const
{
	OutLength = LongestFormStringLen;
	OutUsesFormatArgs = bDoFormsUseFormatArgs;
}

FTextFormatArgumentModifier_EnumForm::FTextFormatArgumentModifier_EnumForm(
	TMap<FTextFormatString, FTextFormat>&& InArgForms, const int32 InLongestFormStringLen, const bool InDoFormsUseFormatArgs )
	: LongestFormStringLen( InLongestFormStringLen )
	, bDoFormsUseFormatArgs( InDoFormsUseFormatArgs )
	, ArgForms( MoveTemp( InArgForms ) )
{
}
```
</div></details>