Add function ApplySafeSubstitution for translation strings with parameters

This commit is contained in:
Henrik Rydgård 2023-07-16 16:16:47 +02:00
parent 755269c90d
commit 8a59ed0062
10 changed files with 64 additions and 23 deletions

View file

@ -368,3 +368,31 @@ std::string UnescapeMenuString(const char *input, char *shortcutChar) {
}
return output;
}
std::string ApplySafeSubstitutions(const char *format, const std::string &string1, const std::string &string2, const std::string &string3) {
size_t formatLen = strlen(format);
std::string output;
output.reserve(formatLen + 20);
for (size_t i = 0; i < formatLen; i++) {
char c = format[i];
if (c != '%') {
output.push_back(c);
continue;
}
if (i >= formatLen - 1) {
break;
}
switch (format[i + 1]) {
case '1':
output += string1; i++;
break;
case '2':
output += string2; i++;
break;
case '3':
output += string3; i++;
break;
}
}
return output;
}

View file

@ -111,3 +111,7 @@ inline void CharArrayFromFormat(char (& out)[Count], const char* format, ...)
// "C:/Windows/winhelp.exe" to "C:/Windows/", "winhelp", ".exe"
bool SplitPath(const std::string& full_path, std::string* _pPath, std::string* _pFilename, std::string* _pExtension);
// Replaces %1, %2, %3 in format with arg1, arg2, arg3.
// Much safer than snprintf and friends.
std::string ApplySafeSubstitutions(const char *format, const std::string &string1, const std::string &string2 = "", const std::string &string3 = "");

View file

@ -531,7 +531,7 @@ void Choice::Draw(UIContext &dc) {
std::string Choice::DescribeText() const {
auto u = GetI18NCategory(I18NCat::UI_ELEMENTS);
return ReplaceAll(u->T("%1 choice"), "%1", text_);
return ApplySafeSubstitutions(u->T("%1 choice"), text_);
}
InfoItem::InfoItem(const std::string &text, const std::string &rightText, LayoutParams *layoutParams)
@ -579,7 +579,7 @@ void InfoItem::Draw(UIContext &dc) {
std::string InfoItem::DescribeText() const {
auto u = GetI18NCategory(I18NCat::UI_ELEMENTS);
return ReplaceAll(ReplaceAll(u->T("%1: %2"), "%1", text_), "%2", rightText_);
return ApplySafeSubstitutions(u->T("%1: %2"), text_, rightText_);
}
ItemHeader::ItemHeader(const std::string &text, LayoutParams *layoutParams)
@ -609,7 +609,7 @@ void ItemHeader::GetContentDimensionsBySpec(const UIContext &dc, MeasureSpec hor
std::string ItemHeader::DescribeText() const {
auto u = GetI18NCategory(I18NCat::UI_ELEMENTS);
return ReplaceAll(u->T("%1 heading"), "%1", text_);
return ApplySafeSubstitutions(u->T("%1 heading"), text_);
}
void BorderView::Draw(UIContext &dc) {
@ -674,7 +674,7 @@ void PopupHeader::Draw(UIContext &dc) {
std::string PopupHeader::DescribeText() const {
auto u = GetI18NCategory(I18NCat::UI_ELEMENTS);
return ReplaceAll(u->T("%1 heading"), "%1", text_);
return ApplySafeSubstitutions(u->T("%1 heading"), text_);
}
void CheckBox::Toggle() {
@ -755,7 +755,7 @@ void CheckBox::Draw(UIContext &dc) {
std::string CheckBox::DescribeText() const {
auto u = GetI18NCategory(I18NCat::UI_ELEMENTS);
std::string text = ReplaceAll(u->T("%1 checkbox"), "%1", text_);
std::string text = ApplySafeSubstitutions(u->T("%1 checkbox"), text_);
if (!smallText_.empty()) {
text += "\n" + smallText_;
}
@ -858,7 +858,7 @@ void Button::GetContentDimensions(const UIContext &dc, float &w, float &h) const
std::string Button::DescribeText() const {
auto u = GetI18NCategory(I18NCat::UI_ELEMENTS);
return ReplaceAll(u->T("%1 button"), "%1", GetText());
return ApplySafeSubstitutions(u->T("%1 button"), GetText());
}
void Button::Click() {
@ -920,7 +920,7 @@ void RadioButton::GetContentDimensions(const UIContext &dc, float &w, float &h)
std::string RadioButton::DescribeText() const {
auto u = GetI18NCategory(I18NCat::UI_ELEMENTS);
return ReplaceAll(u->T("%1 radio button"), "%1", text_);
return ApplySafeSubstitutions(u->T("%1 radio button"), text_);
}
void RadioButton::Click() {
@ -1108,7 +1108,7 @@ void TextEdit::GetContentDimensions(const UIContext &dc, float &w, float &h) con
std::string TextEdit::DescribeText() const {
auto u = GetI18NCategory(I18NCat::UI_ELEMENTS);
return ReplaceAll(u->T("%1 text field"), "%1", GetText());
return ApplySafeSubstitutions(u->T("%1 text field"), GetText());
}
// Handles both windows and unix line endings.
@ -1299,7 +1299,7 @@ void ProgressBar::Draw(UIContext &dc) {
std::string ProgressBar::DescribeText() const {
auto u = GetI18NCategory(I18NCat::UI_ELEMENTS);
float percent = progress_ * 100.0f;
return ReplaceAll(u->T("Progress: %1%"), "%1", StringFromInt((int)percent));
return ApplySafeSubstitutions(u->T("Progress: %1%"), StringFromInt((int)percent));
}
void Spinner::GetContentDimensions(const UIContext &dc, float &w, float &h) const {

View file

@ -228,7 +228,7 @@ static void event_handler_callback(const rc_client_event_t *event, rc_client_t *
const rc_client_game_t *gameInfo = rc_client_get_game_info(g_rcClient);
// TODO: Translation?
std::string title = ReplaceAll(ac->T("Mastered %1"), "%1", gameInfo->title);
std::string title = ApplySafeSubstitutions(ac->T("Mastered %1"), gameInfo->title);
rc_client_user_game_summary_t summary;
rc_client_get_user_game_summary(g_rcClient, &summary);
@ -244,16 +244,18 @@ static void event_handler_callback(const rc_client_event_t *event, rc_client_t *
case RC_CLIENT_EVENT_LEADERBOARD_STARTED:
// A leaderboard attempt has started. The handler may show a message with the leaderboard title and /or description indicating the attempt started.
INFO_LOG(ACHIEVEMENTS, "Leaderboard attempt started: %s", event->leaderboard->title);
g_OSD.Show(OSDType::MESSAGE_INFO, ReplaceAll(ac->T("%1: Leaderboard attempt started"), "%1", event->leaderboard->title), DeNull(event->leaderboard->description), 3.0f);
g_OSD.Show(OSDType::MESSAGE_INFO, ApplySafeSubstitutions(ac->T("%1: Leaderboard attempt started"), event->leaderboard->title), DeNull(event->leaderboard->description), 3.0f);
break;
case RC_CLIENT_EVENT_LEADERBOARD_FAILED:
NOTICE_LOG(ACHIEVEMENTS, "Leaderboard attempt failed: %s", event->leaderboard->title);
g_OSD.Show(OSDType::MESSAGE_INFO, ReplaceAll(ac->T("%1: Leaderboard attempt failed"), "%1", event->leaderboard->title), 3.0f);
g_OSD.Show(OSDType::MESSAGE_INFO, ApplySafeSubstitutions(ac->T("%1: Leaderboard attempt failed"), event->leaderboard->title), 3.0f);
// A leaderboard attempt has failed.
break;
case RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED:
NOTICE_LOG(ACHIEVEMENTS, "Leaderboard result submitted: %s", event->leaderboard->title);
g_OSD.Show(OSDType::MESSAGE_SUCCESS, ReplaceAll(ReplaceAll(ac->T("%1: Submitting leaderboard score: %2!"), "%1", DeNull(event->leaderboard->title)), "%2", DeNull(event->leaderboard->tracker_value)), DeNull(event->leaderboard->description), 3.0f);
g_OSD.Show(OSDType::MESSAGE_SUCCESS,
ApplySafeSubstitutions(ac->T("%1: Submitting leaderboard score: %2!"), DeNull(event->leaderboard->title), DeNull(event->leaderboard->tracker_value)),
DeNull(event->leaderboard->description), 3.0f);
System_PostUIMessage("play_sound", "leaderboard_submitted");
break;
case RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW:

View file

@ -433,7 +433,7 @@ std::string GameButton::DescribeText() const {
return "...";
auto u = GetI18NCategory(I18NCat::UI_ELEMENTS);
return ReplaceAll(u->T("%1 button"), "%1", ginfo->GetTitle());
return ApplySafeSubstitutions(u->T("%1 button"), ginfo->GetTitle());
}
class DirButton : public UI::Button {

View file

@ -338,7 +338,7 @@ void ReportScreen::UpdateCRCInfo() {
if (Reporting::HasCRC(gamePath_)) {
std::string crc = StringFromFormat("%08X", Reporting::RetrieveCRC(gamePath_));
updated = ReplaceAll(rp->T("FeedbackCRCValue", "Disc CRC: %1"), "%1", crc);
updated = ApplySafeSubstitutions(rp->T("FeedbackCRCValue", "Disc CRC: %1"), crc);
} else if (showCRC_) {
updated = rp->T("FeedbackCRCCalculating", "Disc CRC: Calculating...");
}

View file

@ -409,7 +409,7 @@ void SavedataButton::Draw(UIContext &dc) {
std::string SavedataButton::DescribeText() const {
auto u = GetI18NCategory(I18NCat::UI_ELEMENTS);
return ReplaceAll(u->T("%1 button"), "%1", title_) + "\n" + subtitle_;
return ApplySafeSubstitutions(u->T("%1 button"), title_) + "\n" + subtitle_;
}
SavedataBrowser::SavedataBrowser(const Path &path, UI::LayoutParams *layoutParams)
@ -465,9 +465,9 @@ void SavedataBrowser::SetSearchFilter(const std::string &filter) {
if (gameList_)
searchPending_ = true;
if (noMatchView_)
noMatchView_->SetText(ReplaceAll(sa->T("Nothing matching '%1' was found."), "%1", filter));
noMatchView_->SetText(ApplySafeSubstitutions(sa->T("Nothing matching '%1' was found."), filter));
if (searchingView_)
searchingView_->SetText(ReplaceAll(sa->T("Showing matches for '%1'."), "%1", filter));
searchingView_->SetText(ApplySafeSubstitutions(sa->T("Showing matches for '%1'."), filter));
}
void SavedataBrowser::SetSortOption(SavedataSortOption opt) {

View file

@ -130,7 +130,7 @@ void TabbedUIDialogScreenWithGameBackground::ApplySearchFilter() {
// Show an indicator that a filter is applied.
settingTabFilterNotices_[t]->SetVisibility(tabMatches ? UI::V_GONE : UI::V_VISIBLE);
settingTabFilterNotices_[t]->SetText(ReplaceAll(se->T("Filtering settings by '%1'"), "%1", searchFilter_));
settingTabFilterNotices_[t]->SetText(ApplySafeSubstitutions(se->T("Filtering settings by '%1'"), searchFilter_));
UI::View *lastHeading = nullptr;
for (int i = 1; i < tabContents->GetNumSubviews(); ++i) {
@ -152,7 +152,7 @@ void TabbedUIDialogScreenWithGameBackground::ApplySearchFilter() {
matches = matches || tabMatches;
}
noSearchResults_->SetText(ReplaceAll(se->T("No settings matched '%1'"), "%1", searchFilter_));
noSearchResults_->SetText(ApplySafeSubstitutions(se->T("No settings matched '%1'"), searchFilter_));
noSearchResults_->SetVisibility(matches ? UI::V_GONE : UI::V_VISIBLE);
clearSearchChoice_->SetVisibility(searchFilter_.empty() ? UI::V_GONE : UI::V_VISIBLE);
}

View file

@ -26,7 +26,6 @@
%d achievements, %d points = %d achievements, %d points
%1: Leaderboard attempt started = %1: Leaderboard attempt started
%1: Leaderboard attempt failed = %1: Leaderboard attempt failed
%1: Submitting leaderboard score: %2! = %1: Submitting leaderboard score: %2!
Account = Account
Achievements = Achievements
Achievements are disabled = Achievements are disabled
@ -41,17 +40,18 @@ Links = Links
Locked achievements = Locked achievements
Log bad memory accesses = Log bad memory accesses
Mastered %1 = Mastered %1
This feature is not available in Challenge Mode = This feature is not available in Challenge Mode
Save states not available in Challenge Mode = Save states not available in Challenge Mode
Register on www.retroachievements.org = Register on www.retroachievements.org
RetroAchievements are not available for this game = RetroAchievements are not available for this game
RetroAchievements website = RetroAchievements website
Rich Presence = Rich Presence
Save states not available in Challenge Mode = Save states not available in Challenge Mode
Save state loaded without achievement data = Save state loaded without achievement data
Sound Effects = Sound Effects
Statistics = Statistics
Submitted %1 for %2 = Submitted %1 for %2
Syncing achievements data... = Syncing achievements data...
Test Mode = Test Mode
This feature is not available in Challenge Mode = This feature is not available in Challenge Mode
This game has no achievements = This game has no achievements
Unlocked achievements = Unlocked achievements
Unofficial achievements = Unofficial achievements

View file

@ -971,6 +971,12 @@ bool TestEscapeMenuString() {
return true;
}
bool TestSubstitutions() {
std::string output = ApplySafeSubstitutions("%3 %2 %1", "a", "b", "c");
EXPECT_EQ_STR(output, std::string("c b a"));
return true;
}
typedef bool (*TestFunc)();
struct TestItem {
const char *name;
@ -1028,6 +1034,7 @@ TestItem availableTests[] = {
TEST_ITEM(InputMapping),
TEST_ITEM(EscapeMenuString),
TEST_ITEM(VFS),
TEST_ITEM(Substitutions),
};
int main(int argc, const char *argv[]) {