Host: Add plural translation support

Backport of f3aec0c965
This commit is contained in:
Stenzek 2024-05-15 08:11:44 +10:00 committed by Connor McLaughlin
parent 3a0b26225d
commit cfecbf53aa
17 changed files with 3351 additions and 3032 deletions

View File

@ -8,4 +8,4 @@ set -e
# for just updating translations. Saves building it for this action alone.
"$SCRIPTDIR/../../../../tools/retry.sh" sudo apt-get -y install qt6-l10n-tools
PATH=/usr/lib/qt6/bin:$PATH "$SCRIPTDIR/../../../../pcsx2-qt/Translations/update_en_translation.sh"
PATH=/usr/lib/qt6/bin:$PATH "$SCRIPTDIR/../../../../pcsx2-qt/Translations/update_base_translation.sh"

View File

@ -743,6 +743,23 @@ s32 Host::Internal::GetTranslatedStringImpl(
return static_cast<s32>(msg.size());
}
std::string Host::TranslatePluralToString(const char* context, const char* msg, const char* disambiguation, int count)
{
TinyString count_str = TinyString::from_format("{}", count);
std::string ret(msg);
for (;;)
{
std::string::size_type pos = ret.find("%n");
if (pos == std::string::npos)
break;
ret.replace(pos, pos + 2, count_str.view());
}
return ret;
}
//////////////////////////////////////////////////////////////////////////
// Platform specific code
//////////////////////////////////////////////////////////////////////////

View File

@ -259,6 +259,17 @@ int GameListModel::columnCount(const QModelIndex& parent) const
return Column_Count;
}
QString GameListModel::formatTimespan(time_t timespan)
{
// avoid an extra string conversion
const u32 hours = static_cast<u32>(timespan / 3600);
const u32 minutes = static_cast<u32>((timespan % 3600) / 60);
if (hours > 0)
return qApp->translate("GameList", "%n hours", "", hours);
else
return qApp->translate("GameList", "%n minutes", "", minutes);
}
QVariant GameListModel::data(const QModelIndex& index, int role) const
{
if (!index.isValid())
@ -296,7 +307,7 @@ QVariant GameListModel::data(const QModelIndex& index, int role) const
if (ge->total_played_time == 0)
return {};
else
return QString::fromStdString(GameList::FormatTimespan(ge->total_played_time, true));
return formatTimespan(ge->total_played_time);
}
case Column_LastPlayed:

View File

@ -82,6 +82,8 @@ private:
void loadOrGenerateCover(const GameList::Entry* ge);
void invalidateCoverForPath(const std::string& path);
static QString formatTimespan(time_t timespan);
float m_cover_scale = 0.0f;
std::atomic<u32> m_cover_scale_counter{0};
bool m_show_titles_for_covers = false;

View File

@ -95,7 +95,7 @@ static QString getSystemLanguage()
}
// No matches :(
Console.Warning("Couldn't find translation for system language %s, using en instead", locale.toStdString().c_str());
return QStringLiteral("en");
return QStringLiteral("en-US");
}
void QtHost::InstallTranslator(QWidget* dialog_parent)
@ -213,6 +213,11 @@ s32 Host::Internal::GetTranslatedStringImpl(
return static_cast<s32>(translated_size);
}
std::string Host::TranslatePluralToString(const char* context, const char* msg, const char* disambiguation, int count)
{
return qApp->translate(context, msg, disambiguation, count).toStdString();
}
std::vector<std::pair<QString, QString>> QtHost::GetAvailableLanguageList()
{
return {
@ -224,7 +229,7 @@ std::vector<std::pair<QString, QString>> QtHost::GetAvailableLanguageList()
{QStringLiteral("Dansk (da-DK)"), QStringLiteral("da-DK")},
{QStringLiteral("Deutsch (de-DE)"), QStringLiteral("de-DE")},
{QStringLiteral("Ελληνικά (el-GR)"), QStringLiteral("el-GR")},
{QStringLiteral("English (en)"), QStringLiteral("en")},
{QStringLiteral("English (en)"), QStringLiteral("en-US")},
{QStringLiteral("Español (Hispanoamérica) (es-419)"), QStringLiteral("es-419")},
{QStringLiteral("Español (España) (es-ES)"), QStringLiteral("es-ES")},
{QStringLiteral("فارسی (fa-IR)"), QStringLiteral("fa-IR")},

View File

@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="en_US" sourcelanguage="en_US">
<context>
<name>AchievementSettingsWidget</name>
<message numerus="yes">
<location filename="../Settings/AchievementSettingsWidget.cpp" line="134"/>
<location filename="../Settings/AchievementSettingsWidget.cpp" line="141"/>
<source>%n seconds</source>
<translation>
<numerusform>%n second</numerusform>
<numerusform>%n seconds</numerusform>
</translation>
</message>
</context>
<context>
<name>Achievements</name>
<message numerus="yes">
<location filename="../../pcsx2/Achievements.cpp" line="1021"/>
<source>You have unlocked {} of %n achievements</source>
<comment>Achievement popup</comment>
<translation>
<numerusform>You have unlocked {} of %n achievements</numerusform>
<numerusform>You have unlocked {} of %n achievements</numerusform>
</translation>
</message>
<message numerus="yes">
<location filename="../../pcsx2/Achievements.cpp" line="1024"/>
<source>and earned {} of %n points</source>
<comment>Achievement popup</comment>
<translation>
<numerusform>and earned {} of %n points</numerusform>
<numerusform>and earned {} of %n points</numerusform>
</translation>
</message>
<message numerus="yes">
<location filename="../../pcsx2/Achievements.cpp" line="1109"/>
<source>%n achievements</source>
<comment>Mastery popup</comment>
<translation>
<numerusform>%n achievement</numerusform>
<numerusform>%n achievements</numerusform>
</translation>
</message>
<message numerus="yes">
<location filename="../../pcsx2/Achievements.cpp" line="1111"/>
<source>%n points</source>
<comment>Mastery popup</comment>
<translation>
<numerusform>%n point</numerusform>
<numerusform>%n points</numerusform>
</translation>
</message>
</context>
<context>
<name>GameList</name>
<message numerus="yes">
<location filename="../GameList/GameListModel.cpp" line="268"/>
<location filename="../../pcsx2/GameList.cpp" line="1142"/>
<source>%n hours</source>
<translation>
<numerusform>%n hour</numerusform>
<numerusform>%n hours</numerusform>
</translation>
</message>
<message numerus="yes">
<location filename="../GameList/GameListModel.cpp" line="270"/>
<location filename="../../pcsx2/GameList.cpp" line="1144"/>
<source>%n minutes</source>
<translation>
<numerusform>%n minute</numerusform>
<numerusform>%n minutes</numerusform>
</translation>
</message>
</context>
<context>
<name>InputBindingWidget</name>
<message numerus="yes">
<location filename="../Settings/InputBindingWidget.cpp" line="73"/>
<source>%n bindings</source>
<translation>
<numerusform>%n binding</numerusform>
<numerusform>%n bindings</numerusform>
</translation>
</message>
</context>
<context>
<name>Patch</name>
<message numerus="yes">
<location filename="../../pcsx2/Patch.cpp" line="698"/>
<source>%n GameDB patches are active.</source>
<comment>OSD Message</comment>
<translation>
<numerusform>%n GameDB patch is active.</numerusform>
<numerusform>%n GameDB patches are active.</numerusform>
</translation>
</message>
<message numerus="yes">
<location filename="../../pcsx2/Patch.cpp" line="705"/>
<source>%n game patches are active.</source>
<comment>OSD Message</comment>
<translation>
<numerusform>%n game patch is active.</numerusform>
<numerusform>%n game patches are active.</numerusform>
</translation>
</message>
<message numerus="yes">
<location filename="../../pcsx2/Patch.cpp" line="712"/>
<source>%n cheat patches are active.</source>
<comment>OSD Message</comment>
<translation>
<numerusform>%n cheat patch is active.</numerusform>
<numerusform>%n cheat patches are active.</numerusform>
</translation>
</message>
</context>
</TS>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,10 @@
@echo off
set QTBIN=..\..\deps\bin
set SRCDIRS=../ ../../pcsx2/
set OPTS=-tr-function-alias QT_TRANSLATE_NOOP+=TRANSLATE,QT_TRANSLATE_NOOP+=TRANSLATE_SV,QT_TRANSLATE_NOOP+=TRANSLATE_STR,QT_TRANSLATE_NOOP+=TRANSLATE_FS,QT_TRANSLATE_N_NOOP3+=TRANSLATE_FMT,QT_TRANSLATE_NOOP+=TRANSLATE_NOOP,translate+=TRANSLATE_PLURAL_STR,translate+=TRANSLATE_PLURAL_FS
"%QTBIN%\lupdate.exe" %SRCDIRS% %OPTS% -no-obsolete -source-language en -ts pcsx2-qt_en.ts
pause

View File

@ -0,0 +1,9 @@
#!/bin/bash
SCRIPTDIR=$(dirname "${BASH_SOURCE[0]}")
OPTS="-tr-function-alias QT_TRANSLATE_NOOP+=TRANSLATE,QT_TRANSLATE_NOOP+=TRANSLATE_SV,QT_TRANSLATE_NOOP+=TRANSLATE_STR,QT_TRANSLATE_NOOP+=TRANSLATE_FS,QT_TRANSLATE_N_NOOP3+=TRANSLATE_FMT,QT_TRANSLATE_NOOP+=TRANSLATE_NOOP,translate+=TRANSLATE_PLURAL_STR,translate+=TRANSLATE_PLURAL_FS"
SRCDIRS=$(realpath "$SCRIPTDIR/..")/\ $(realpath "$SCRIPTDIR/../../pcsx2")/
OUTDIR=$(realpath "$SCRIPTDIR")
lupdate $SRCDIRS $OPTS -no-obsolete -source-language en -ts "$OUTDIR/pcsx2-qt_en.ts"

View File

@ -3,8 +3,7 @@
set QTBIN=..\..\deps\bin
set SRCDIRS=../ ../../pcsx2/
set OPTS=-tr-function-alias QT_TRANSLATE_NOOP+=TRANSLATE,QT_TRANSLATE_NOOP+=TRANSLATE_SV,QT_TRANSLATE_NOOP+=TRANSLATE_STR,QT_TRANSLATE_NOOP+=TRANSLATE_FS,QT_TRANSLATE_N_NOOP3+=TRANSLATE_FMT,QT_TRANSLATE_NOOP+=TRANSLATE_NOOP
set OPTS=-tr-function-alias QT_TRANSLATE_NOOP+=TRANSLATE,QT_TRANSLATE_NOOP+=TRANSLATE_SV,QT_TRANSLATE_NOOP+=TRANSLATE_STR,QT_TRANSLATE_NOOP+=TRANSLATE_FS,QT_TRANSLATE_N_NOOP3+=TRANSLATE_FMT,QT_TRANSLATE_NOOP+=TRANSLATE_NOOP,translate+=TRANSLATE_PLURAL_STR,translate+=TRANSLATE_PLURAL_FS -pluralonly -no-obsolete
"%QTBIN%\lupdate.exe" %SRCDIRS% %OPTS% -no-obsolete -source-language en -ts pcsx2-qt_en.ts
pause
"%QTBIN%\lupdate.exe" %SRCDIRS% %OPTS% -no-obsolete -source-language en_US -ts pcsx2-qt_en-US.ts
start %QTBIN%\linguist.exe %~dp0\pcsx2-qt_en-US.ts

View File

@ -2,8 +2,8 @@
SCRIPTDIR=$(dirname "${BASH_SOURCE[0]}")
OPTS="-tr-function-alias QT_TRANSLATE_NOOP+=TRANSLATE,QT_TRANSLATE_NOOP+=TRANSLATE_SV,QT_TRANSLATE_NOOP+=TRANSLATE_STR,QT_TRANSLATE_NOOP+=TRANSLATE_FS,QT_TRANSLATE_N_NOOP3+=TRANSLATE_FMT,QT_TRANSLATE_NOOP+=TRANSLATE_NOOP"
OPTS="-tr-function-alias QT_TRANSLATE_NOOP+=TRANSLATE,QT_TRANSLATE_NOOP+=TRANSLATE_SV,QT_TRANSLATE_NOOP+=TRANSLATE_STR,QT_TRANSLATE_NOOP+=TRANSLATE_FS,QT_TRANSLATE_N_NOOP3+=TRANSLATE_FMT,QT_TRANSLATE_NOOP+=TRANSLATE_NOOP,translate+=TRANSLATE_PLURAL_STR,translate+=TRANSLATE_PLURAL_FS -pluralonly -no-obsolete"
SRCDIRS=$(realpath "$SCRIPTDIR/..")/\ $(realpath "$SCRIPTDIR/../../pcsx2")/
OUTDIR=$(realpath "$SCRIPTDIR")
lupdate $SRCDIRS $OPTS -no-obsolete -source-language en -ts "$OUTDIR/pcsx2-qt_en.ts"
lupdate $SRCDIRS $OPTS -no-obsolete -source-language en_US -ts "$OUTDIR/pcsx2-qt_en-US.ts"

View File

@ -1016,9 +1016,14 @@ void Achievements::DisplayAchievementSummary()
std::string summary;
if (s_game_summary.num_core_achievements > 0)
{
summary = fmt::format(TRANSLATE_FS("Achievements", "You have unlocked {0} of {1} achievements, and earned {2} of {3} points."),
s_game_summary.num_unlocked_achievements, s_game_summary.num_core_achievements, s_game_summary.points_unlocked,
s_game_summary.points_core);
summary = fmt::format(
TRANSLATE_FS("Achievements", "{0}, {1}."),
SmallString::from_format(TRANSLATE_PLURAL_FS("Achievements", "You have unlocked {} of %n achievements",
"Achievement popup", s_game_summary.num_core_achievements),
s_game_summary.num_unlocked_achievements),
SmallString::from_format(TRANSLATE_PLURAL_FS("Achievements", "and earned {} of %n points", "Achievement popup",
s_game_summary.points_core),
s_game_summary.points_unlocked));
}
else
{
@ -1099,8 +1104,11 @@ void Achievements::HandleGameCompleteEvent(const rc_client_event_t* event)
if (EmuConfig.Achievements.Notifications)
{
std::string title = fmt::format(TRANSLATE_FS("Achievements", "Mastered {}"), s_game_title);
std::string message = fmt::format(TRANSLATE_FS("Achievements", "{0} achievements, {1} points"),
s_game_summary.num_unlocked_achievements, s_game_summary.points_unlocked);
std::string message = fmt::format(
TRANSLATE_FS("Achievements", "{0}, {1}"),
TRANSLATE_PLURAL_STR("Achievements", "%n achievements", "Mastery popup",
s_game_summary.num_unlocked_achievements),
TRANSLATE_PLURAL_STR("Achievements", "%n points", "Mastery popup", s_game_summary.num_unlocked_achievements));
MTGS::RunOnGSThread([title = std::move(title), message = std::move(message), icon = s_game_icon]() {
if (ImGuiManager::InitializeFullscreenUI())

View File

@ -1139,9 +1139,9 @@ std::string GameList::FormatTimespan(std::time_t timespan, bool long_format)
else
{
if (hours > 0)
ret = fmt::format(TRANSLATE_FS("GameList", "{} hours"), hours);
ret.assign(TRANSLATE_PLURAL_STR("GameList", "%n hours", "", hours));
else
ret = fmt::format(TRANSLATE_FS("GameList", "{} minutes"), minutes);
ret.assign(TRANSLATE_PLURAL_STR("GameList", "%n minutes", "", minutes));
}
return ret;

View File

@ -42,6 +42,9 @@ namespace Host
/// Returns a localized version of the specified string within the specified context.
std::string TranslateToString(const std::string_view& context, const std::string_view& msg);
/// Returns a localized version of the specified string within the specified context, adjusting for plurals using %n.
std::string TranslatePluralToString(const char* context, const char* msg, const char* disambiguation, int count);
/// Clears the translation cache. All previously used strings should be considered invalid.
void ClearTranslationCache();
@ -164,6 +167,10 @@ namespace Host
#define TRANSLATE_SV(context, msg) Host::TranslateToStringView(context, msg)
#define TRANSLATE_STR(context, msg) Host::TranslateToString(context, msg)
#define TRANSLATE_FS(context, msg) fmt::runtime(Host::TranslateToStringView(context, msg))
#define TRANSLATE_PLURAL_STR(context, msg, disambiguation, count) \
Host::TranslatePluralToString(context, msg, disambiguation, count)
#define TRANSLATE_PLURAL_FS(context, msg, disambiguation, count) \
fmt::runtime(Host::TranslatePluralToString(context, msg, disambiguation, count))
// Does not translate the string at runtime, but allows the UI to in its own way.
#define TRANSLATE_NOOP(context, msg) msg

View File

@ -93,10 +93,9 @@ static bool CanPause()
const float delta = static_cast<float>(Common::Timer::ConvertValueToSeconds(time - s_last_pause_time));
if (delta < PAUSE_INTERVAL)
{
Host::AddIconOSDMessage(
"PauseCooldown", ICON_FA_CLOCK,
fmt::format(TRANSLATE_FS("Hotkeys", "You cannot pause until another {:.1f} seconds have passed."),
PAUSE_INTERVAL - delta),
Host::AddIconOSDMessage("PauseCooldown", ICON_FA_CLOCK,
TRANSLATE_PLURAL_STR("Hotkeys", "You cannot pause until another %n second(s) have passed.",
"", static_cast<int>(std::ceil(PAUSE_INTERVAL - delta))),
Host::OSD_QUICK_DURATION);
return false;
}

View File

@ -689,27 +689,27 @@ void Patch::UpdateActivePatches(bool reload_enabled_list, bool verbose, bool ver
s_override_aspect_ratio.reset();
s_override_interlace_mode.reset();
std::string message;
SmallString message;
u32 gp_count = 0;
if (EmuConfig.EnablePatches)
{
gp_count = EnablePatches(s_gamedb_patches, EnablePatchList());
if (gp_count > 0)
fmt::format_to(std::back_inserter(message), TRANSLATE_FS("Patch", "{} GameDB patches"), gp_count);
message.append(TRANSLATE_PLURAL_STR("Patch", "%n GameDB patches are active.", "OSD Message", gp_count));
}
const u32 p_count = EnablePatches(s_game_patches, s_enabled_patches);
if (p_count > 0)
{
fmt::format_to(std::back_inserter(message), TRANSLATE_FS("Patch", "{}{} game patches"),
message.empty() ? "" : ", ", p_count);
message.append_format("{}{}", message.empty() ? "" : "\n",
TRANSLATE_PLURAL_STR("Patch", "%n game patches are active.", "OSD Message", p_count));
}
const u32 c_count = EmuConfig.EnableCheats ? EnablePatches(s_cheat_patches, s_enabled_cheats) : 0;
if (c_count > 0)
{
fmt::format_to(std::back_inserter(message), TRANSLATE_FS("Patch", "{}{} cheat patches"),
message.empty() ? "" : ", ", c_count);
message.append_format("{}{}", message.empty() ? "" : "\n",
TRANSLATE_PLURAL_STR("Patch", "%n cheat patches are active.", "OSD Message", c_count));
}
// Display message on first boot when we load patches.
@ -719,8 +719,7 @@ void Patch::UpdateActivePatches(bool reload_enabled_list, bool verbose, bool ver
{
if (!message.empty())
{
Host::AddIconOSDMessage("LoadPatches", ICON_FA_BAND_AID,
fmt::format(TRANSLATE_FS("Patch", "{} are active."), message), Host::OSD_INFO_DURATION);
Host::AddIconOSDMessage("LoadPatches", ICON_FA_BAND_AID, message, Host::OSD_INFO_DURATION);
}
else
{

View File

@ -191,6 +191,23 @@ s32 Host::Internal::GetTranslatedStringImpl(
return static_cast<s32>(msg.size());
}
std::string Host::TranslatePluralToString(const char* context, const char* msg, const char* disambiguation, int count)
{
TinyString count_str = TinyString::from_format("{}", count);
std::string ret(msg);
for (;;)
{
std::string::size_type pos = ret.find("%n");
if (pos == std::string::npos)
break;
ret.replace(pos, pos + 2, count_str.view());
}
return ret;
}
void Host::OnAchievementsLoginRequested(Achievements::LoginRequestReason reason)
{
}