Singular and Plural Text in Unreal C++ with FText::Format
Use the plural argument modifier in FText::Format so an Unreal ability tooltip reads 1 second and 5 seconds correctly in every language.
An ability tooltip has to say "1 second" and "5 seconds" from the same line of code. Unreal makes that decision inside the format pattern with an argument modifier, so the count and the word it changes stay together.
Here is the text we want, at both durations:
Grants +5% Movement Speed for 1 second
Grants +5% Movement Speed for 5 secondsAnd the function that produces it:
FText UMovementSpeedEffect::GetDescriptionText() const
{
FFormatNamedArguments Args;
Args.Add(TEXT("Percent"), SpeedBonusPercent);
Args.Add(TEXT("Duration"), DurationSeconds);
const FText Pattern = NSLOCTEXT(
"AbilityEffects",
"MovementSpeedBuffFormat",
"Grants +{Percent}% Movement Speed for {Duration} {Duration}|plural(one=second,other=seconds)"
);
return FText::Format(Pattern, Args);
}This builds on named arguments and FText::Format. The new part is everything after the pipe.
The Ternary That Breaks
The instinctive fix looks like this:
const FString Unit = DurationSeconds == 1 ? TEXT("second") : TEXT("seconds");It is correct in English and wrong almost everywhere else, for two separate reasons.
Not every language has two forms. Unreal follows the CLDR plural rules, which define six possible categories: zero, one, two, few, many, and other. English uses two of them. Polish uses four, and picks between them based on the last two digits — 2 takes one form, 5 takes another, 22 goes back to the first. A single boolean cannot express that.
The decision is in the wrong file. A translator receives the text you marked up for localization. If the choice between "second" and "seconds" lives in a C++ ternary, it is not in the text they receive, and no amount of translation can fix it.
The modifier moves that decision into the pattern, where it becomes part of what gets translated.
1. Add the Count as a Numeric Argument
FFormatNamedArguments Args;
Args.Add(TEXT("Percent"), SpeedBonusPercent);
Args.Add(TEXT("Duration"), DurationSeconds);DurationSeconds is an int32, and it matters that it stays one. The plural modifier inspects the numeric value of the argument to decide which form to use, so the count has to reach the formatter as a number rather than as text.
2. Write the Pattern
const FText Pattern = NSLOCTEXT(
"AbilityEffects",
"MovementSpeedBuffFormat",
"Grants +{Percent}% Movement Speed for {Duration} {Duration}|plural(one=second,other=seconds)"
);Duration appears twice, and each appearance does a different job:
- The first prints the number itself.
- The second runs a modifier on that number and prints a word.
An argument modifier is a pipe, a function name, and its arguments. The plural forms are key-value pairs, one per CLDR category:
{Duration}|plural(one=second,other=seconds)Because the whole thing sits inside NSLOCTEXT, the forms travel with the text. A translator working on this line sees the modifier and can supply the categories their language actually needs.
3. Format It
return FText::Format(Pattern, Args);Unreal resolves each placeholder against the current culture:
Duration = 1
{Duration} -> 1
{Duration}|plural(one=second,other=seconds) -> second
Duration = 5
{Duration} -> 5
{Duration}|plural(one=second,other=seconds) -> secondsOne pattern, one call, both sentences.
Adding a language does not mean touching this function. A Polish translation of
the same pattern adds the few and many forms; the C++ keeps passing a
number and keeps working.
Common Mistakes
Branching in C++ instead of in the pattern
Any if or ternary that picks between player-facing words is a decision a translator can no longer reach. Put the alternatives in the pattern and let the formatter choose.
Converting the count before adding it
FText::AsNumber(DurationSeconds) returns text. The plural modifier needs a number to inspect, so converting the count before adding it takes away the thing the modifier works from. Add the raw number instead.
When you genuinely need custom number formatting, use two arguments — one formatted for display, one left numeric for the decision:
Args.Add(TEXT("DurationText"), FText::AsNumber(DurationSeconds, &NumberFormat));
Args.Add(TEXT("DurationCount"), DurationSeconds);for {DurationText} {DurationCount}|plural(one=second,other=seconds)Expecting zero to fire at zero
English maps 0 to other, so a zero form written into an English pattern never appears — 0 renders as "0 seconds". The zero category exists for languages whose rules define it, not as a special case for the number zero.
Passing a float for a whole number
A float duration prints as 1.0 rather than 1. Use an int32 when the value is a whole number of seconds, and round before formatting if your gameplay value happens to be a float.
Leaving out other
other is the fallback every culture can fall back to. Always include it, even when a language seems to need only one form.
The Takeaway
Give the formatter the count as a number, then let the pattern decide the word with |plural. The sentence stays in one localizable string, and languages with more than two plural forms get what they need without a single change to your C++.
The same modifier syntax covers the neighbouring cases too: {Place}|ordinal(one=st,two=nd,few=rd,other=th) for "You came 3rd", |gender(...) for languages that inflect around grammatical gender, and a leading backtick to escape a brace or pipe you want printed literally.
For more detail, see Epic's Text Formatting reference and the Text Localization overview.
Know someone who'd find this useful?
New notes
Hear when I post something new.
Choose game development, web development, or both, and I'll send you an occasional update.