Add function ApplySafeSubstitution for translation strings with parameters
This commit is contained in:
parent
755269c90d
commit
8a59ed0062
10 changed files with 64 additions and 23 deletions
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 = "");
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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...");
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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[]) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue