1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-12-06

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

FormatText 関数について

テキスト ローカライゼーション

テキスト フォーマットは、実際のテキストを挿入してフォーマット プレース ホルダを置き換えるローカライズ可能なフォーマット パターンを使用してテキストを組み合わせることで、ローカライズを容易にします。

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

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

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

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

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

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

TextFormatArgumentModifier クラスの拡張

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

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
910bc5ec805400fd226418a7b585189a.png
配列指定でフォームを選択します
Int 以外の変数を渡した場合や、配列の存在しないインデックスを渡した場合は何も表示されません(実装を書けばエラーとすることもできる)

実装コード
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;
};
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 ) )
{
}

キーペアで指定する

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

3e2134444e07090b22a733d505d4ea7e.png
0e5422b86a2a9895d5106bb268525051.png
キーペアを指定しフォームを選択します
キー指定が存在しなかった場合に "_" の値を使うオプションがあります
(Enum 値は FormatText 関数に直接は接続できないので、Int 変換を挟む必要があります)

実装コード
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;
};
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 ) )
{
}
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?