diff --git a/Makefile.am b/Makefile.am index d87d99916..c8456aec7 100644 --- a/Makefile.am +++ b/Makefile.am @@ -50,6 +50,9 @@ EXTRA_DIST = \ src/gui/bitmaps/undo.h \ src/gui/bitmaps/redo.h \ src/gui/bitmaps/gambitrc.rc \ + po/gambit.pot \ + po/es.po \ + po/fr.po \ contrib/games/2s2x2x2.efg \ contrib/games/2x2x2.efg \ contrib/games/4cards.efg \ @@ -347,7 +350,8 @@ bin_PROGRAMS = gambit endif # Define VERSION for all C++ compilations and set include paths -AM_CPPFLAGS = -I$(top_srcdir)/src -DVERSION=\"$(VERSION)\" +AM_CPPFLAGS = -I$(top_srcdir)/src -DVERSION=\"$(VERSION)\" \ + -DLOCALEDIR=\"$(LOCALEDIR)\" AM_CXXFLAGS = ${LLVM_CXXFLAGS} @@ -379,6 +383,8 @@ gambit_SOURCES = \ src/gui/edittext.h \ src/gui/dlabout.cc \ src/gui/dlabout.h \ + src/gui/dlpreferences.cc \ + src/gui/dlpreferences.h \ src/gui/dleditmove.cc \ src/gui/dleditmove.h \ src/gui/dleditnode.cc \ @@ -457,6 +463,56 @@ endif src/gui/bitmaps/gambitrc.o: src/gui/bitmaps/gambitrc.rc $(WINDRES) $(WX_CXXFLAGS) -I$(srcdir)/src/gui/bitmaps -o $@ $< +#----------------------------------------------------------------------- +# Translations +# +# GUI strings are extracted with xgettext (maintainer action, "make update-po"), +# kept as hand-edited po/.po files, and compiled to GNU mo catalogs with +# msgfmt at build time. The mo files are installed under $(LOCALEDIR), which +# app.cc registers with wxLocale so that the "_()" macro translates the UI. +# When msgfmt is unavailable, no catalogs are built/installed and the interface +# falls back to the built-in English strings. +#----------------------------------------------------------------------- + +PO_LANGS = es fr +PO_CATALOGS = $(PO_LANGS:%=$(top_builddir)/po/%.mo) + +if HAVE_MSGFMT +all-local: $(PO_CATALOGS) + +$(top_builddir)/po/%.mo: $(top_srcdir)/po/%.po + $(MKDIR_P) $(top_builddir)/po + $(MSGFMT) --check -o $@ $< + +install-data-local: $(PO_CATALOGS) + @for lang in $(PO_LANGS); do \ + $(MKDIR_P) $(DESTDIR)$(LOCALEDIR)/$$lang/LC_MESSAGES || exit 1; \ + $(INSTALL_DATA) $(top_builddir)/po/$$lang.mo \ + $(DESTDIR)$(LOCALEDIR)/$$lang/LC_MESSAGES/gambit.mo || exit 1; \ + done +endif + +if HAVE_XGETTEXT +.PHONY: update-po +update-po: po/gambit.pot + @for lang in $(PO_LANGS); do \ + test -f po/$$lang.po || cp -f po/gambit.pot po/$$lang.po; \ + msgmerge --update --quiet po/$$lang.po po/gambit.pot >/dev/null 2>&1 || true; \ + done + +po/gambit.pot: $(gambit_SOURCES) + $(MKDIR_P) po + $(XGETTEXT) --keyword=_ --keyword=wxPLURAL:1,2 --add-comments=TRANSLATORS: \ + --from-code=UTF-8 --language=C++ -o $@ $(gambit_SOURCES) + $(XGETTEXT) --keyword=_ --keyword=wxPLURAL:1,2 --add-comments=TRANSLATORS: \ + --from-code=UTF-8 --language=C++ -j -o $@ $(wildcard src/gui/*.h) +endif + +uninstall-local: + @for lang in $(PO_LANGS); do \ + rm -f $(DESTDIR)$(LOCALEDIR)/$$lang/LC_MESSAGES/gambit.mo; \ + done + osx-bundle: make all -rm -rf Gambit.app @@ -476,6 +532,12 @@ osx-dmg: osx-bundle msw-msi: -mkdir installer cp gambit* installer +if HAVE_MSGFMT + $(MSGFMT) --check -o $(top_builddir)/po/es.mo $(top_srcdir)/po/es.po + $(MSGFMT) --check -o $(top_builddir)/po/fr.mo $(top_srcdir)/po/fr.po + cp $(top_builddir)/po/es.mo installer/es.mo + cp $(top_builddir)/po/fr.mo installer/fr.mo +endif candle build_support/msw/gambit.wxs light -ext WixUIExtension gambit.wixobj diff --git a/build_support/msw/gambit.wxs.in b/build_support/msw/gambit.wxs.in index f43976bd5..89e2ee0e7 100644 --- a/build_support/msw/gambit.wxs.in +++ b/build_support/msw/gambit.wxs.in @@ -68,7 +68,18 @@ - + + + + + + + + + + + + @@ -101,10 +112,22 @@ + + + + + + + + + + + + diff --git a/configure.ac b/configure.ac index 2fd0947e1..88681c759 100644 --- a/configure.ac +++ b/configure.ac @@ -143,6 +143,27 @@ AC_SUBST(WX_LIBS) AC_SUBST(WX_LIBS_STATIC) AC_SUBST(WX_CXXFLAGS) +dnl--------------------------------------- +dnl Checking for gettext tools & locale dir +dnl--------------------------------------- +AC_ARG_VAR([LOCALEDIR], [Directory for installed locale data files]) +test -n "$LOCALEDIR" || LOCALEDIR='${datadir}/locale' +AC_SUBST(LOCALEDIR) + +AC_CHECK_PROG([MSGFMT], [msgfmt], [msgfmt]) +if test -n "$MSGFMT"; then + AM_CONDITIONAL([HAVE_MSGFMT], [true]) +else + AM_CONDITIONAL([HAVE_MSGFMT], [false]) +fi + +AC_CHECK_PROG([XGETTEXT], [xgettext], [xgettext]) +if test -n "$XGETTEXT"; then + AM_CONDITIONAL([HAVE_XGETTEXT], [true]) +else + AM_CONDITIONAL([HAVE_XGETTEXT], [false]) +fi + REZFLAGS=`echo $CXXFLAGS $WX_CXXFLAGS | sed 's/-mthreads//g' | sed 's/-g//g' | sed 's/-O. / /g' | sed 's/-I/--include-dir /g'` AC_SUBST(REZFLAGS) diff --git a/po/es.po b/po/es.po new file mode 100644 index 000000000..504dc97bb --- /dev/null +++ b/po/es.po @@ -0,0 +1,162 @@ +# Spanish translations for Gambit. +# Copyright (C) 2026 The Gambit Project +# This file is distributed under the same license as the Gambit package. +# Fernando P. , 2026. +# +msgid "" +msgstr "" +"Project-Id-Version: Gambit\n" +"Report-Msgid-Bugs-To: https://github.com/gambitproject/gambit\n" +"POT-Creation-Date: 2026-08-29 00:00+0000\n" +"PO-Revision-Date: 2026-08-29 00:00+0000\n" +"Last-Translator: Fernando P. \n" +"Language-Team: Spanish \n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. Main menu bar +msgid "&File" +msgstr "&Archivo" + +msgid "&Edit" +msgstr "&Editar" + +msgid "&View" +msgstr "&Ver" + +msgid "&Format" +msgstr "&Formato" + +msgid "&Tools" +msgstr "&Herramientas" + +msgid "&Help" +msgstr "&Ayuda" + +#. File menu +msgid "&Extensive game" +msgstr "Juego &extensivo" + +msgid "&Strategic game" +msgstr "Juego &estratégico" + +msgid "&New" +msgstr "&Nuevo" + +msgid "&Open\tCtrl-O" +msgstr "&Abrir\tCtrl-O" + +msgid "&Save\tCtrl-S" +msgstr "&Guardar\tCtrl-S" + +msgid "Save &as\tShift-Ctrl-S" +msgstr "Guardar &como\tShift-Ctrl-S" + +msgid "&Export" +msgstr "&Exportar" + +msgid "Page Se&tup" +msgstr "Configur&ación de página" + +msgid "Print Pre&view" +msgstr "&Vista preliminar" + +msgid "&Print\tCtrl-P" +msgstr "&Imprimir\tCtrl-P" + +msgid "&Close\tCtrl-W" +msgstr "&Cerrar\tCtrl-W" + +msgid "E&xit\tCtrl-Q" +msgstr "&Salir\tCtrl-Q" + +#. Edit menu +msgid "&Undo\tCtrl-Z" +msgstr "&Deshacer\tCtrl-Z" + +msgid "&Redo\tShift-Ctrl-Z" +msgstr "&Rehacer\tShift-Ctrl-Z" + +msgid "&Game" +msgstr "&Juego" + +msgid "&Preferences" +msgstr "&Preferencias" + +#. View menu +msgid "&Profiles" +msgstr "&Perfiles" + +msgid "Zoom &In\tCtrl-+" +msgstr "A&mpliar\tCtrl-+" + +msgid "Zoom &Out\tCtrl--" +msgstr "&Reducir\tCtrl--" + +msgid "&Actual Size\tCtrl-0" +msgstr "Tamaño &real\tCtrl-0" + +msgid "Zoom to &Fit" +msgstr "Ajustar a la &ventana" + +msgid "La&bels" +msgstr "&Etiquetas" + +msgid "&Font" +msgstr "&Fuente" + +msgid "&Layout" +msgstr "&Diseño" + +#. Tools menu +msgid "&Dominance" +msgstr "&Dominancia" + +msgid "&Equilibrium" +msgstr "&Equilibrio" + +msgid "&QRE" +msgstr "&QRE" + +#. Help menu / dialogs +msgid "&About Gambit" +msgstr "&Acerca de Gambit" + +msgid "Welcome to Gambit" +msgstr "Bienvenido a Gambit" + +msgid "Preferences" +msgstr "Preferencias" + +msgid "Language:" +msgstr "Idioma:" + +msgid "Appearance:" +msgstr "Apariencia:" + +msgid "System (default)" +msgstr "Sistema (predeterminado)" + +msgid "English" +msgstr "Inglés" + +msgid "Spanish" +msgstr "Español" + +msgid "French" +msgstr "Francés" + +msgid "Light" +msgstr "Claro" + +msgid "Dark" +msgstr "Oscuro" + +msgid "OK" +msgstr "Aceptar" + +msgid "Cancel" +msgstr "Cancelar" diff --git a/po/fr.po b/po/fr.po new file mode 100644 index 000000000..c3522c20c --- /dev/null +++ b/po/fr.po @@ -0,0 +1,162 @@ +# French translations for Gambit. +# Copyright (C) 2026 The Gambit Project +# This file is distributed under the same license as the Gambit package. +# Marie D. , 2026. +# +msgid "" +msgstr "" +"Project-Id-Version: Gambit\n" +"Report-Msgid-Bugs-To: https://github.com/gambitproject/gambit\n" +"POT-Creation-Date: 2026-08-29 00:00+0000\n" +"PO-Revision-Date: 2026-08-29 00:00+0000\n" +"Last-Translator: Marie D. \n" +"Language-Team: French \n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#. Main menu bar +msgid "&File" +msgstr "&Fichier" + +msgid "&Edit" +msgstr "&Édition" + +msgid "&View" +msgstr "&Affichage" + +msgid "&Format" +msgstr "&Format" + +msgid "&Tools" +msgstr "&Outils" + +msgid "&Help" +msgstr "&Aide" + +#. File menu +msgid "&Extensive game" +msgstr "Jeu &extensif" + +msgid "&Strategic game" +msgstr "Jeu &stratégique" + +msgid "&New" +msgstr "&Nouveau" + +msgid "&Open\tCtrl-O" +msgstr "&Ouvrir\tCtrl-O" + +msgid "&Save\tCtrl-S" +msgstr "&Enregistrer\tCtrl-S" + +msgid "Save &as\tShift-Ctrl-S" +msgstr "Enregistrer &sous\tShift-Ctrl-S" + +msgid "&Export" +msgstr "&Exporter" + +msgid "Page Se&tup" +msgstr "&Mise en page" + +msgid "Print Pre&view" +msgstr "&Aperçu" + +msgid "&Print\tCtrl-P" +msgstr "&Imprimer\tCtrl-P" + +msgid "&Close\tCtrl-W" +msgstr "&Fermer\tCtrl-W" + +msgid "E&xit\tCtrl-Q" +msgstr "&Quitter\tCtrl-Q" + +#. Edit menu +msgid "&Undo\tCtrl-Z" +msgstr "&Annuler\tCtrl-Z" + +msgid "&Redo\tShift-Ctrl-Z" +msgstr "&Rétablir\tShift-Ctrl-Z" + +msgid "&Game" +msgstr "&Jeu" + +msgid "&Preferences" +msgstr "&Préférences" + +#. View menu +msgid "&Profiles" +msgstr "&Profils" + +msgid "Zoom &In\tCtrl-+" +msgstr "&Zoom avant\tCtrl-+" + +msgid "Zoom &Out\tCtrl--" +msgstr "Zoom &arrière\tCtrl--" + +msgid "&Actual Size\tCtrl-0" +msgstr "Taille &réelle\tCtrl-0" + +msgid "Zoom to &Fit" +msgstr "Ajuster à la &fenêtre" + +msgid "La&bels" +msgstr "&Étiquettes" + +msgid "&Font" +msgstr "&Police" + +msgid "&Layout" +msgstr "&Disposition" + +#. Tools menu +msgid "&Dominance" +msgstr "&Dominance" + +msgid "&Equilibrium" +msgstr "&Équilibre" + +msgid "&QRE" +msgstr "&QRE" + +#. Help menu / dialogs +msgid "&About Gambit" +msgstr "À &propos de Gambit" + +msgid "Welcome to Gambit" +msgstr "Bienvenue dans Gambit" + +msgid "Preferences" +msgstr "Préférences" + +msgid "Language:" +msgstr "Langue :" + +msgid "Appearance:" +msgstr "Apparence :" + +msgid "System (default)" +msgstr "Système (par défaut)" + +msgid "English" +msgstr "Anglais" + +msgid "Spanish" +msgstr "Espagnol" + +msgid "French" +msgstr "Français" + +msgid "Light" +msgstr "Clair" + +msgid "Dark" +msgstr "Sombre" + +msgid "OK" +msgstr "OK" + +msgid "Cancel" +msgstr "Annuler" diff --git a/po/gambit.pot b/po/gambit.pot new file mode 100644 index 000000000..32d9a87fe --- /dev/null +++ b/po/gambit.pot @@ -0,0 +1,19 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR The Gambit Project +# This file is distributed under the same license as the Gambit package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: Gambit\n" +"Report-Msgid-Bugs-To: https://github.com/gambitproject/gambit\n" +"POT-Creation-Date: 2026-08-29 00:00+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" diff --git a/src/gui/app.cc b/src/gui/app.cc index 309ceed6b..3b6b84999 100644 --- a/src/gui/app.cc +++ b/src/gui/app.cc @@ -27,6 +27,7 @@ #include #include #include +#include #include "games.h" @@ -74,6 +75,12 @@ wxBEGIN_EVENT_TABLE(Application, wxApp) EVT_TIMER(wxID_ANY, Application::OnSplas // m_fileHistory.Save(config); wxConfigBase::Get()->Read(_T("/General/CurrentDirectory"), &m_currentDir, _T("")); + // Apply the persisted language preference (if any), so that every dialog/window + // constructed from here on is localized. + wxString languagePref; + wxConfigBase::Get()->Read(_T("/General/Language"), &languagePref, _T("System")); + SetLanguage(languagePref); + // Process command line arguments, if any. for (int i = 1; i < argc; i++) { LoadFile(argv[i], nullptr); @@ -119,6 +126,55 @@ void Application::DismissSplash() m_splash = nullptr; } +bool Application::SetLanguage(const wxString &p_language) +{ + std::string code = p_language.ToStdString(); + if (code == "System" || code.empty()) { + code = "en"; + } + + wxLanguage lang = wxLANGUAGE_ENGLISH; + if (code == "en") { + lang = wxLANGUAGE_ENGLISH; + } + else if (code == "es") { + lang = wxLANGUAGE_SPANISH; + } + else if (code == "fr") { + lang = wxLANGUAGE_FRENCH; + } + else { + lang = wxLANGUAGE_UNKNOWN; + return false; + } + + // Recreate the locale on every call: wxLocale::Init() may only be invoked once + // per object, so a runtime language change (via the Preferences dialog) must build + // a fresh locale rather than re-initializing the existing one. The new locale is + // only committed once it has been initialized successfully. + auto *newLocale = new wxLocale; + +#ifndef LOCALEDIR +#define LOCALEDIR "share/locale" +#endif + newLocale->AddCatalogLookupPathPrefix(LOCALEDIR); + if (!newLocale->Init(lang)) { + delete newLocale; + return false; + } + newLocale->AddCatalog("gambit"); + + delete m_locale; + m_locale = newLocale; + return true; +} + +Application::~Application() +{ + delete m_locale; + m_locale = nullptr; +} + namespace { /// A simple single-button error notice, styled like GameFrame's CloseWarningDialog: diff --git a/src/gui/app.h b/src/gui/app.h index bdb02b64b..973fc19ea 100644 --- a/src/gui/app.h +++ b/src/gui/app.h @@ -26,6 +26,7 @@ #include #include // for wxConfig #include // for wxFileHistory +#include // for wxLocale #include namespace Gambit::GUI { @@ -46,6 +47,7 @@ class Application final : public wxApp { wxSplashScreen *m_splash{nullptr}; wxStopWatch m_splashTimer; wxTimer m_splashDismissTimer; + wxLocale *m_locale{nullptr}; bool OnInit() override; void DismissSplash(); @@ -55,11 +57,21 @@ class Application final : public wxApp { public: Application() = default; - ~Application() override = default; + ~Application() override; const wxString &GetCurrentDir() { return m_currentDir; } void SetCurrentDir(const wxString &p_dir); + //! Applies the given language code ("en", "es", "fr", or "System") by + //! (re)initializing the application locale and loading the gambit catalog. + //! Returns true on success. + bool SetLanguage(const wxString &p_language); + //! Short code of the language currently in effect. + wxString GetLanguage() const + { + return m_locale ? m_locale->GetCanonicalName().BeforeFirst('_') : wxString("en"); + } + wxString GetHistoryFile(int index) const { return m_fileHistory.GetHistoryFile(index); } void AddMenu(wxMenu *p_menu) { diff --git a/src/gui/dleditmove.cc b/src/gui/dleditmove.cc index e1d6d1d89..93bd1bd29 100644 --- a/src/gui/dleditmove.cc +++ b/src/gui/dleditmove.cc @@ -311,7 +311,7 @@ void ActionPanel::Rebuild() } row.deleteButton = - new wxButton(this, wxID_ANY, row.isDeleted ? wxUniChar(0x21BA) : wxUniChar(0x2715), + new wxButton(this, wxID_ANY, row.isDeleted ? wxUniChar(0x21BA) : wxUniChar('X'), wxDefaultPosition, buttonSize); row.deleteButton->SetToolTip(row.isDeleted ? _("Restore action") : _("Delete action")); row.deleteButton->Enable(row.isDeleted || activeCount > 1); @@ -646,8 +646,8 @@ void EditMoveDialog::OnOK(wxCommandEvent &p_event) ValidateDistribution(probs); } catch (ValueException &) { - wxRichMessageDialog(this, "Probabilities must be nonnegative numbers summing to one.", "Error", - wxOK | wxCENTRE | wxICON_ERROR) + wxRichMessageDialog(this, _("Probabilities must be nonnegative numbers summing to one."), + _("Error"), wxOK | wxCENTRE | wxICON_ERROR) .ShowModal(); return; } diff --git a/src/gui/dleditstrategies.cc b/src/gui/dleditstrategies.cc index 46bcce58f..a786e147f 100644 --- a/src/gui/dleditstrategies.cc +++ b/src/gui/dleditstrategies.cc @@ -272,7 +272,7 @@ void StrategyPanel::Rebuild() } row.deleteButton = - new wxButton(this, wxID_ANY, row.isDeleted ? wxUniChar(0x21BA) : wxUniChar(0x2715), + new wxButton(this, wxID_ANY, row.isDeleted ? wxUniChar(0x21BA) : wxUniChar('X'), wxDefaultPosition, buttonSize); row.deleteButton->SetToolTip(row.isDeleted ? _("Restore strategy") : _("Delete strategy")); row.deleteButton->Enable(row.isDeleted || activeCount > 1); diff --git a/src/gui/dlefglayout.cc b/src/gui/dlefglayout.cc index 1427aa9bd..83f2dea57 100644 --- a/src/gui/dlefglayout.cc +++ b/src/gui/dlefglayout.cc @@ -65,7 +65,7 @@ LayoutNodesPanel::LayoutNodesPanel(wxWindow *p_parent, const TreeRenderConfig &p auto *topSizer = new wxBoxSizer(wxVERTICAL); - auto *nodeSizer = new wxStaticBoxSizer(wxVERTICAL, this, _T("Drawing nodes")); + auto *nodeSizer = new wxStaticBoxSizer(wxVERTICAL, this, _("Drawing nodes")); auto *tokenSizer = new wxFlexGridSizer(2); @@ -183,7 +183,7 @@ LayoutBranchesPanel::LayoutBranchesPanel(wxWindow *p_parent, const TreeRenderCon styleBoxSizer->Add(styleSizer, 1, wxALL | wxEXPAND, S); topSizer->Add(styleBoxSizer, 0, wxALL | wxALIGN_CENTER, S); - auto *lengthSizer = new wxStaticBoxSizer(wxHORIZONTAL, this, _T("size of branches")); + auto *lengthSizer = new wxStaticBoxSizer(wxHORIZONTAL, this, _("size of branches")); auto *gridSizer = new wxFlexGridSizer(2); gridSizer->AddGrowableCol(1); diff --git a/src/gui/dlexcept.h b/src/gui/dlexcept.h index fcf762cbb..ad5580462 100644 --- a/src/gui/dlexcept.h +++ b/src/gui/dlexcept.h @@ -33,7 +33,7 @@ class ExceptionDialog final : public wxRichMessageDialog { public: ExceptionDialog(wxWindow *p_parent, const std::string &p_message) : wxRichMessageDialog(p_parent, wxString(p_message.c_str(), *wxConvCurrent), - wxT("Internal exception in Gambit"), wxOK | wxCENTRE | wxICON_ERROR) + _("Internal exception in Gambit"), wxOK | wxCENTRE | wxICON_ERROR) { } }; diff --git a/src/gui/dlgameprop.cc b/src/gui/dlgameprop.cc index 4a4926245..c7bf11d8a 100644 --- a/src/gui/dlgameprop.cc +++ b/src/gui/dlgameprop.cc @@ -343,7 +343,7 @@ void PlayerPanel::Rebuild() const bool canDelete = row.isDeleted || (blockedReason.empty() && activeCount > 1); row.deleteButton = - new wxButton(this, wxID_ANY, row.isDeleted ? wxUniChar(0x21BA) : wxUniChar(0x2715), + new wxButton(this, wxID_ANY, row.isDeleted ? wxUniChar(0x21BA) : wxUniChar('X'), wxDefaultPosition, buttonSize); if (row.isDeleted) { row.deleteButton->SetToolTip(_("Restore player")); diff --git a/src/gui/dllogit.h b/src/gui/dllogit.h index d6462b2cc..b9e130607 100644 --- a/src/gui/dllogit.h +++ b/src/gui/dllogit.h @@ -344,7 +344,7 @@ template class LogitDialog final : public wxDialog { m_runner = std::make_unique>(this, m_doc->GetGame()); if (m_runner->Run() != wxTHREAD_NO_ERROR) { m_runner.reset(); - m_statusText->SetLabel(wxT("Failed to launch computation.")); + m_statusText->SetLabel(_("Failed to launch computation.")); m_statusText->SetForegroundColour(*wxRED); m_okButton->Enable(true); } @@ -377,15 +377,15 @@ template class LogitDialog final : public wxDialog { } if (m_stopRequested) { - m_statusText->SetLabel(wxT("The computation has been stopped.")); + m_statusText->SetLabel(_("The computation has been stopped.")); m_statusText->SetForegroundColour(wxColour(196, 128, 0)); } else if (p_event.GetInt() == 0) { - m_statusText->SetLabel(wxT("The computation has completed.")); + m_statusText->SetLabel(_("The computation has completed.")); m_statusText->SetForegroundColour(wxColour(0, 192, 0)); } else { - m_statusText->SetLabel(wxT("The computation ended abnormally.")); + m_statusText->SetLabel(_("The computation ended abnormally.")); m_statusText->SetForegroundColour(*wxRED); } @@ -435,7 +435,7 @@ template class LogitDialog final : public wxDialog { public: LogitDialog(wxWindow *p_parent, const std::shared_ptr &p_doc) - : wxDialog(p_parent, wxID_ANY, wxT("Compute quantal response equilibria"), wxDefaultPosition, + : wxDialog(p_parent, wxID_ANY, _("Compute quantal response equilibria"), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER), m_doc(p_doc), m_list(new LogitGrid(this, m_doc)) { @@ -446,7 +446,7 @@ template class LogitDialog final : public wxDialog { auto *startSizer = new wxBoxSizer(wxHORIZONTAL); m_statusText = - new wxStaticText(this, wxID_STATIC, wxT("The computation is currently in progress.")); + new wxStaticText(this, wxID_STATIC, _("The computation is currently in progress.")); m_statusText->SetForegroundColour(*wxBLUE); startSizer->Add(m_statusText, 0, wxALL | wxALIGN_CENTER, S); @@ -462,7 +462,7 @@ template class LogitDialog final : public wxDialog { sizer->Add(m_list, 1, wxALL | wxEXPAND, S); auto *buttonSizer = new wxBoxSizer(wxHORIZONTAL); - m_saveButton = new wxButton(this, wxID_SAVE, wxT("Save correspondence to .csv file")); + m_saveButton = new wxButton(this, wxID_SAVE, _("Save correspondence to .csv file")); m_saveButton->Enable(false); buttonSizer->Add(m_saveButton, 0, wxALL | wxALIGN_CENTER, S); m_saveButton->Bind(wxEVT_BUTTON, &LogitDialog::OnSave, this); diff --git a/src/gui/dlnash.cc b/src/gui/dlnash.cc index 47c2a47a0..82afb2680 100644 --- a/src/gui/dlnash.cc +++ b/src/gui/dlnash.cc @@ -33,17 +33,17 @@ #include "dlnash.h" namespace Gambit::GUI { -static wxString s_recommended(wxT("with Gambit's recommended method")); -static wxString s_enumpure(wxT("by looking for pure strategy equilibria")); -static wxString s_enummixed(wxT("by enumerating extreme points")); -static wxString s_enumpoly(wxT("by solving systems of polynomial equations")); -static wxString s_gnm(wxT("by global Newton tracing")); -static wxString s_ipa(wxT("by iterated polymatrix approximation")); -static wxString s_lp(wxT("by solving a linear program")); -static wxString s_lcp(wxT("by solving a linear complementarity program")); -static wxString s_liap(wxT("by minimizing the Lyapunov function")); -static wxString s_logit(wxT("by tracing logit equilibria")); -static wxString s_simpdiv(wxT("by simplicial subdivision")); +static wxString RecommendedMethodName() { return _("with Gambit's recommended method"); } +static wxString EnumPureMethodName() { return _("by looking for pure strategy equilibria"); } +static wxString EnumMixedMethodName() { return _("by enumerating extreme points"); } +static wxString EnumPolyMethodName() { return _("by solving systems of polynomial equations"); } +static wxString GnmMethodName() { return _("by global Newton tracing"); } +static wxString IpaMethodName() { return _("by iterated polymatrix approximation"); } +static wxString LpMethodName() { return _("by solving a linear program"); } +static wxString LcpMethodName() { return _("by solving a linear complementarity program"); } +static wxString LiapMethodName() { return _("by minimizing the Lyapunov function"); } +static wxString LogitMethodName() { return _("by tracing logit equilibria"); } +static wxString SimpdivMethodName() { return _("by simplicial subdivision"); } namespace { @@ -61,7 +61,7 @@ NashEquilibriumTarget GetTarget(int p_selection) NashMethodSpec ResolveMethod(const wxString &p_method, NashEquilibriumTarget p_target, const Game &p_game) { - if (p_method == s_recommended) { + if (p_method == RecommendedMethodName()) { if (p_target == NashEquilibriumTarget::One) { if (p_game->NumPlayers() == 2 && p_game->IsConstSum()) { return LPNashSpec{}; @@ -76,37 +76,37 @@ NashMethodSpec ResolveMethod(const wxString &p_method, NashEquilibriumTarget p_t } return EnumMixedNashSpec{}; } - if (p_method == s_enumpure) { + if (p_method == EnumPureMethodName()) { return EnumPureNashSpec{}; } - if (p_method == s_enummixed) { + if (p_method == EnumMixedMethodName()) { return EnumMixedNashSpec{}; } - if (p_method == s_enumpoly) { + if (p_method == EnumPolyMethodName()) { if (p_target == NashEquilibriumTarget::One) { return EnumPolyNashSpec{.stopAfter = 1}; } return EnumPolyNashSpec{}; } - if (p_method == s_gnm) { + if (p_method == GnmMethodName()) { return GNMNashSpec{}; } - if (p_method == s_ipa) { + if (p_method == IpaMethodName()) { return IPANashSpec{}; } - if (p_method == s_lp) { + if (p_method == LpMethodName()) { return LPNashSpec{}; } - if (p_method == s_lcp) { + if (p_method == LcpMethodName()) { return LCPNashSpec{}; } - if (p_method == s_liap) { + if (p_method == LiapMethodName()) { return LiapNashSpec{}; } - if (p_method == s_logit) { + if (p_method == LogitMethodName()) { return LogitNashSpec{}; } - if (p_method == s_simpdiv) { + if (p_method == SimpdivMethodName()) { if (p_target == NashEquilibriumTarget::One) { return SimpdivNashSpec{.startingPoints = 1}; } @@ -212,34 +212,34 @@ wxString MethodDescription(const NashMethodSpec &p_method) return std::visit( [](const Method &method) { if constexpr (std::is_same_v) { - return wxT("in pure strategies"); + return _("in pure strategies"); } else if constexpr (std::is_same_v) { - return wxT("by enumeration of mixed-strategy extreme points"); + return _("by enumeration of mixed-strategy extreme points"); } else if constexpr (std::is_same_v) { - return wxT("by solving polynomial systems"); + return _("by solving polynomial systems"); } else if constexpr (std::is_same_v) { - return wxT("by global Newton tracing"); + return _("by global Newton tracing"); } else if constexpr (std::is_same_v) { - return wxT("by iterated polymatrix approximation"); + return _("by iterated polymatrix approximation"); } else if constexpr (std::is_same_v) { - return wxT("by solving a linear program"); + return _("by solving a linear program"); } else if constexpr (std::is_same_v) { - return wxT("by solving a linear complementarity program"); + return _("by solving a linear complementarity program"); } else if constexpr (std::is_same_v) { - return wxT("by function minimization"); + return _("by function minimization"); } else if constexpr (std::is_same_v) { - return wxT("by logit tracing"); + return _("by logit tracing"); } else { - return wxT("by simplicial subdivision"); + return _("by simplicial subdivision"); } }, p_method); @@ -251,39 +251,39 @@ wxString ParameterDescription(const NashMethodSpec &p_method) [](const Method &method) { if constexpr (std::is_same_v) { if (method.stopAfter.has_value() && method.stopAfter.value() == 1) { - return wxString::Format(" (stop after one equilibrium; maximum regret %.4g)", + return wxString::Format(_(" (stop after one equilibrium; maximum regret %.4g)"), method.maxRegret); } - return wxString::Format(" (all supports; maximum regret %.4g)", method.maxRegret); + return wxString::Format(_(" (all supports; maximum regret %.4g)"), method.maxRegret); } else if constexpr (std::is_same_v) { return wxString::Format( - " (%d perturbation; ending lambda %.4g; %d steps per support cell; local Newton " - "every %d steps, at most %d iterations)", + _(" (%d perturbation; ending lambda %.4g; %d steps per support cell; local Newton " + "every %d steps, at most %d iterations)"), method.perturbations, method.lambdaEnd, method.steps, method.localNewtonInterval, method.localNewtonMaxIterations); } else if constexpr (std::is_same_v) { - return wxString::Format(" (%d perturbation)", method.perturbations); + return wxString::Format(_(" (%d perturbation)"), method.perturbations); } else if constexpr (std::is_same_v) { - return wxString(wxT(" (all accessible equilibria; unlimited recursion depth)")); + return _(" (all accessible equilibria; unlimited recursion depth)"); } else if constexpr (std::is_same_v) { - return wxString::Format(" (%d random starting points; at most %d iterations; maximum " - "regret %.4g)", + return wxString::Format(_(" (%d random starting points; at most %d iterations; maximum " + "regret %.4g)"), method.startingPoints, method.maxIterations, method.maxRegret); } else if constexpr (std::is_same_v) { return wxString::Format( - " (maximum regret %.4g; initial step %.4g; maximum acceleration %.4g)", + _(" (maximum regret %.4g; initial step %.4g; maximum acceleration %.4g)"), method.maxRegret, method.firstStep, method.maxAcceleration); } else if constexpr (std::is_same_v) { std::ostringstream regret; regret << method.maxRegret; - return wxString::Format(" (%d random starting points with denominator %d; grid resize " - "factor %d; maximum regret ", + return wxString::Format(_(" (%d random starting points with denominator %d; grid resize " + "factor %d; maximum regret "), method.startingPoints, method.randomDenominator, method.gridResize) + wxString(regret.str()) + wxT(")"); @@ -298,7 +298,7 @@ wxString ParameterDescription(const NashMethodSpec &p_method) } // namespace NashChoiceDialog::NashChoiceDialog(wxWindow *p_parent, const std::shared_ptr &p_doc) - : wxDialog(p_parent, wxID_ANY, wxT("Compute Nash equilibria"), wxDefaultPosition, wxDefaultSize, + : wxDialog(p_parent, wxID_ANY, _("Compute Nash equilibria"), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER), m_doc(p_doc) { @@ -307,15 +307,15 @@ NashChoiceDialog::NashChoiceDialog(wxWindow *p_parent, const std::shared_ptrGetGame()->NumPlayers() == 2) { - wxString countChoices[] = {wxT("Compute one Nash equilibrium"), - wxT("Compute some Nash equilibria"), - wxT("Compute all Nash equilibria")}; + wxString countChoices[] = {_("Compute one Nash equilibrium"), + _("Compute some Nash equilibria"), + _("Compute all Nash equilibria")}; m_countChoice = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 3, countChoices); } else { - wxString countChoices[] = {wxT("Compute one Nash equilibrium"), - wxT("Compute some Nash equilibria")}; + wxString countChoices[] = {_("Compute one Nash equilibrium"), + _("Compute some Nash equilibria")}; m_countChoice = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 2, countChoices); } @@ -326,12 +326,12 @@ NashChoiceDialog::NashChoiceDialog(wxWindow *p_parent, const std::shared_ptrAdd(m_countChoice, 0, wxALL | wxEXPAND, S); if (p_doc->GetGame()->NumPlayers() == 2 && m_doc->GetGame()->IsConstSum()) { - wxString methodChoices[] = {s_recommended, s_lp, s_simpdiv, s_logit, s_enumpoly}; + wxString methodChoices[] = {RecommendedMethodName(), LpMethodName(), SimpdivMethodName(), LogitMethodName(), EnumPolyMethodName()}; m_methodChoice = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 5, methodChoices); } else { - wxString methodChoices[] = {s_recommended, s_simpdiv, s_logit, s_enumpoly}; + wxString methodChoices[] = {RecommendedMethodName(), SimpdivMethodName(), LogitMethodName(), EnumPolyMethodName()}; m_methodChoice = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 4, methodChoices); } @@ -339,7 +339,7 @@ NashChoiceDialog::NashChoiceDialog(wxWindow *p_parent, const std::shared_ptrAdd(m_methodChoice, 0, wxALL | wxEXPAND, S); if (m_doc->GetGame()->IsTree()) { - wxString repChoices[] = {wxT("using the extensive game"), wxT("using the strategic game")}; + wxString repChoices[] = {_("using the extensive game"), _("using the strategic game")}; m_repChoice = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 2, repChoices); m_repChoice->SetSelection(0); topSizer->Add(m_repChoice, 0, wxALL | wxEXPAND, S); @@ -369,29 +369,29 @@ NashChoiceDialog::NashChoiceDialog(wxWindow *p_parent, const std::shared_ptrClear(); - m_methodChoice->Append(s_recommended); + m_methodChoice->Append(RecommendedMethodName()); if (p_event.GetSelection() == 0) { if (m_doc->GetGame()->NumPlayers() == 2 && m_doc->GetGame()->IsConstSum()) { - m_methodChoice->Append(s_lp); + m_methodChoice->Append(LpMethodName()); } - m_methodChoice->Append(s_simpdiv); - m_methodChoice->Append(s_logit); - m_methodChoice->Append(s_enumpoly); + m_methodChoice->Append(SimpdivMethodName()); + m_methodChoice->Append(LogitMethodName()); + m_methodChoice->Append(EnumPolyMethodName()); } else if (p_event.GetSelection() == 1) { if (m_doc->GetGame()->NumPlayers() == 2) { - m_methodChoice->Append(s_lcp); + m_methodChoice->Append(LcpMethodName()); } - m_methodChoice->Append(s_enumpure); - m_methodChoice->Append(s_liap); - m_methodChoice->Append(s_gnm); - m_methodChoice->Append(s_ipa); - m_methodChoice->Append(s_enumpoly); + m_methodChoice->Append(EnumPureMethodName()); + m_methodChoice->Append(LiapMethodName()); + m_methodChoice->Append(GnmMethodName()); + m_methodChoice->Append(IpaMethodName()); + m_methodChoice->Append(EnumPolyMethodName()); } else { if (m_doc->GetGame()->NumPlayers() == 2) { - m_methodChoice->Append(s_enummixed); + m_methodChoice->Append(EnumMixedMethodName()); } } m_methodChoice->SetSelection(0); @@ -430,7 +430,7 @@ NashComputationSpec NashChoiceDialog::GetComputation() const ? NashRepresentation::Strategic : NashRepresentation::Behavior; return {representation, target, std::move(method), - m_methodChoice->GetStringSelection() == s_recommended}; + m_methodChoice->GetStringSelection() == RecommendedMethodName()}; } std::shared_ptr NashChoiceDialog::GetCommand() const @@ -448,17 +448,17 @@ std::shared_ptr NashChoiceDialog::GetCommand() const wxString count; switch (computation.target) { case NashEquilibriumTarget::One: - count = wxT("One equilibrium "); + count = _("One equilibrium "); break; case NashEquilibriumTarget::Some: - count = wxT("Some equilibria "); + count = _("Some equilibria "); break; case NashEquilibriumTarget::All: - count = wxT("All equilibria "); + count = _("All equilibria "); break; } const wxString representation = - useBehavior ? wxT(" in extensive game") : wxT(" in strategic game"); + useBehavior ? _(" in extensive game") : _(" in strategic game"); output->SetComputationSpec(computation); output->SetCommand(ExternalCommand(computation)); output->SetDescription(count + MethodDescription(computation.method) + diff --git a/src/gui/dlnashmon.cc b/src/gui/dlnashmon.cc index 091439fbd..0e3912d9d 100644 --- a/src/gui/dlnashmon.cc +++ b/src/gui/dlnashmon.cc @@ -320,27 +320,27 @@ class NashMonitorDialog final : public wxDialog { void SetStatusRunning() const { - m_statusText->SetLabel(wxT("The computation is currently in progress.")); + m_statusText->SetLabel(_("The computation is currently in progress.")); m_statusText->SetForegroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); m_stopButton->Enable(true); m_okButton->Enable(false); } void SetStatusStopping() const { - m_statusText->SetLabel(wxT("Stopping computation...")); + m_statusText->SetLabel(_("Stopping computation...")); m_statusText->SetForegroundColour(wxColour(196, 128, 0)); m_stopButton->Enable(false); } void SetStatusStopped() const { - m_statusText->SetLabel(wxT("The computation has been stopped.")); + m_statusText->SetLabel(_("The computation has been stopped.")); m_statusText->SetForegroundColour(wxColour(196, 128, 0)); m_okButton->Enable(true); m_stopButton->Enable(false); } void SetStatusFinishedNormally() const { - m_statusText->SetLabel(wxT("The computation has completed.")); + m_statusText->SetLabel(_("The computation has completed.")); m_statusText->SetForegroundColour(wxColour(0, 192, 0)); m_okButton->Enable(true); m_stopButton->Enable(false); @@ -361,7 +361,7 @@ class NashMonitorDialog final : public wxDialog { NashMonitorDialog::NashMonitorDialog(wxWindow *p_parent, const std::shared_ptr &p_doc, const std::shared_ptr &p_command) - : wxDialog(p_parent, wxID_ANY, wxT("Computing Nash equilibria"), wxDefaultPosition, + : wxDialog(p_parent, wxID_ANY, _("Computing Nash equilibria"), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER), m_doc(p_doc), m_output(p_command) { @@ -371,10 +371,11 @@ NashMonitorDialog::NashMonitorDialog(wxWindow *p_parent, auto *startSizer = new wxBoxSizer(wxHORIZONTAL); - m_statusText = new wxStaticText(this, wxID_STATIC, "The computation is currently in progress."); + m_statusText = new wxStaticText(this, wxID_STATIC, _("The computation is currently in progress.")); startSizer->Add(m_statusText, 0, wxALL | wxALIGN_CENTER, S); - m_countText = new wxStaticText(this, wxID_STATIC, wxT("Number of equilibria found so far: 0 ")); + m_countText = + new wxStaticText(this, wxID_STATIC, _("Number of equilibria found so far: 0 ")); startSizer->Add(m_countText, 0, wxALL | wxALIGN_CENTER, S); m_stopButton = @@ -447,7 +448,7 @@ void NashMonitorDialog::Start(const std::shared_ptr &p_command) std::make_unique(this, m_doc->GetGame(), std::move(*solver)); if (m_threadRunner->Run() != wxTHREAD_NO_ERROR) { m_threadRunner.reset(); - SetStatusFinishedAbnormally("Failed to launch solver thread."); + SetStatusFinishedAbnormally(_("Failed to launch solver thread.")); return; } SetStatusRunning(); @@ -469,13 +470,13 @@ void NashMonitorDialog::Start(const std::shared_ptr &p_command) SetStatusRunning(); break; case ExternalProcessRunner::RunnerStartResult::LaunchFailed: - SetStatusFinishedAbnormally("Failed to launch solver."); + SetStatusFinishedAbnormally(_("Failed to launch solver.")); break; case ExternalProcessRunner::RunnerStartResult::NoOutputPipe: - SetStatusFinishedAbnormally("Solver launched, but I/O redirection failed."); + SetStatusFinishedAbnormally(_("Solver launched, but I/O redirection failed.")); break; case ExternalProcessRunner::RunnerStartResult::StdinWriteFailed: - SetStatusFinishedAbnormally("Failed to send input to solver."); + SetStatusFinishedAbnormally(_("Failed to send input to solver.")); break; } } @@ -486,7 +487,7 @@ void NashMonitorDialog::OnRunnerProfile(wxThreadEvent &p_event) std::visit([this](const auto &p) { AddProfile(*m_output, p); }, profile); m_doc->DoAnalysisOutputChanged(); wxString label; - label << wxT("Number of equilibria found so far: ") << m_output->NumProfiles(); + label << _("Number of equilibria found so far: ") << m_output->NumProfiles(); m_countText->SetLabel(label); } @@ -510,7 +511,7 @@ void NashMonitorDialog::OnRunnerFinished(wxThreadEvent &p_event) } else { SetStatusFinishedAbnormally( - wxString::Format("The computation ended abnormally (code %d)", p_event.GetInt())); + wxString::Format(_("The computation ended abnormally (code %d)"), p_event.GetInt())); } } diff --git a/src/gui/dlnewtable.cc b/src/gui/dlnewtable.cc index f2f5aadea..9f1954a88 100644 --- a/src/gui/dlnewtable.cc +++ b/src/gui/dlnewtable.cc @@ -17,7 +17,7 @@ constexpr int ID_FIRST_STRATEGY_COUNT = wxID_HIGHEST + 500; } // namespace NewTableDialog::NewTableDialog(wxWindow *parent) - : wxDialog(parent, wxID_ANY, wxT("New strategic-form game"), wxDefaultPosition, wxDefaultSize, + : wxDialog(parent, wxID_ANY, _("New strategic-form game"), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) { CreateControls(); @@ -41,7 +41,7 @@ std::vector NewTableDialog::GetDimensions() const void NewTableDialog::CreateControls() { - auto *playerCountLabel = new wxStaticText(this, wxID_ANY, wxT("Number of players:")); + auto *playerCountLabel = new wxStaticText(this, wxID_ANY, _("Number of players:")); m_playerCountCtrl = new wxSpinCtrl(this, ID_PLAYER_COUNT); m_playerCountCtrl->SetRange(2, 16); m_playerCountCtrl->SetValue(2); @@ -65,7 +65,7 @@ void NewTableDialog::LayoutControls() formSizer->Add(playerCountLabel, 0, wxALIGN_CENTER_VERTICAL); formSizer->Add(m_playerCountCtrl, 0); - auto *strategyBox = new wxStaticBoxSizer(wxVERTICAL, this, wxT("Strategies per player")); + auto *strategyBox = new wxStaticBoxSizer(wxVERTICAL, this, _("Strategies per player")); strategyBox->Add(m_strategyRowsSizer, 0, wxEXPAND | wxALL, FromDIP(8)); auto *buttonSizer = CreateStdDialogButtonSizer(wxOK | wxCANCEL); @@ -97,7 +97,7 @@ void NewTableDialog::RebuildStrategyControls() grid->AddGrowableCol(1, 1); for (int i = 0; i < playerCount; ++i) { - auto *label = new wxStaticText(this, wxID_ANY, wxString::Format(wxT("Player %d:"), i + 1)); + auto *label = new wxStaticText(this, wxID_ANY, wxString::Format(_("Player %d:"), i + 1)); auto *ctrl = new wxSpinCtrl(this, ID_FIRST_STRATEGY_COUNT + i); ctrl->SetRange(1, 99); diff --git a/src/gui/dlpreferences.cc b/src/gui/dlpreferences.cc new file mode 100644 index 000000000..c6db922ce --- /dev/null +++ b/src/gui/dlpreferences.cc @@ -0,0 +1,109 @@ +// +// This file is part of Gambit +// Copyright (c) 1994-2026, The Gambit Project (https://www.gambit-project.org) +// +// FILE: src/gui/dlpreferences.cc +// Implementation of the Preferences dialog (interface language). +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// + +#ifndef WX_PRECOMP +#include +#endif // WX_PRECOMP +#include + +#include "dlpreferences.h" +#include "app.h" + +namespace Gambit::GUI { + +BEGIN_EVENT_TABLE(PreferencesDialog, wxDialog) +EVT_BUTTON(wxID_OK, PreferencesDialog::OnOK) +END_EVENT_TABLE() + +PreferencesDialog::PreferencesDialog(wxWindow *p_parent) + : wxDialog(p_parent, wxID_ANY, _("Preferences"), wxDefaultPosition, wxDefaultSize, + wxDEFAULT_DIALOG_STYLE) +{ + const int S = FromDIP(5); + + // Read the currently active language so the dialog opens with the current + // choice selected. + wxString language = wxGetApp().GetLanguage(); + if (language.empty() || language == "System") { + language = "System"; + } + + auto *sizer = new wxBoxSizer(wxVERTICAL); + + auto *grid = new wxFlexGridSizer(2, S, S); + grid->AddGrowableCol(1, 1); + + grid->Add(new wxStaticText(this, wxID_STATIC, _("Language:")), 0, + wxALIGN_CENTER_VERTICAL | wxALL, S); + m_languageChoice = new wxChoice(this, wxID_ANY); + m_languageChoice->Append(_("System (default)")); + m_languageChoice->Append(_("English")); + m_languageChoice->Append(_("Spanish")); + m_languageChoice->Append(_("French")); + if (language == "en") { + m_languageChoice->SetSelection(1); + } + else if (language == "es") { + m_languageChoice->SetSelection(2); + } + else if (language == "fr") { + m_languageChoice->SetSelection(3); + } + else { + m_languageChoice->SetSelection(0); + } + grid->Add(m_languageChoice, 0, wxEXPAND | wxALL, S); + + sizer->Add(grid, 0, wxALL | wxEXPAND, S); + + sizer->Add(CreateStdDialogButtonSizer(wxOK | wxCANCEL), 0, wxALL | wxEXPAND, S); + + SetSizerAndFit(sizer); + CentreOnParent(); +} + +void PreferencesDialog::OnOK(wxCommandEvent &p_event) +{ + // Language + wxString language = "System"; + switch (m_languageChoice->GetSelection()) { + case 1: + language = "en"; + break; + case 2: + language = "es"; + break; + case 3: + language = "fr"; + break; + default: + language = "System"; + break; + } + if (wxGetApp().SetLanguage(language) && wxConfigBase::Get()) { + wxConfigBase::Get()->Write(_T("/General/Language"), language); + } + + p_event.Skip(); +} + +} // namespace Gambit::GUI diff --git a/src/gui/dlpreferences.h b/src/gui/dlpreferences.h new file mode 100644 index 000000000..9501298e3 --- /dev/null +++ b/src/gui/dlpreferences.h @@ -0,0 +1,49 @@ +// +// This file is part of Gambit +// Copyright (c) 1994-2026, The Gambit Project (https://www.gambit-project.org) +// +// FILE: src/gui/dlpreferences.h +// Declaration of the Preferences dialog (interface language). +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// + +#ifndef GAMBIT_GUI_DLPREFERENCES_H +#define GAMBIT_GUI_DLPREFERENCES_H + +#include +#include + +namespace Gambit::GUI { + +//! +//! A modal dialog letting the user choose the interface language. The choice +//! persists to the application configuration and takes effect immediately on +//! confirmation. +//! +class PreferencesDialog final : public wxDialog { +public: + PreferencesDialog(wxWindow *p_parent); + +private: + wxChoice *m_languageChoice{nullptr}; + + void OnOK(wxCommandEvent &); + wxDECLARE_EVENT_TABLE(); +}; + +} // namespace Gambit::GUI + +#endif // GAMBIT_GUI_DLPREFERENCES_H diff --git a/src/gui/gameframe.cc b/src/gui/gameframe.cc index 238b62013..b995858e0 100644 --- a/src/gui/gameframe.cc +++ b/src/gui/gameframe.cc @@ -55,6 +55,7 @@ #include "dlnash.h" #include "dllogit.h" #include "dlabout.h" +#include "dlpreferences.h" #include "dlefglayout.h" #include "dlefglegend.h" @@ -227,6 +228,7 @@ EVT_MENU_RANGE(wxID_FILE1, wxID_FILE9, GameFrame::OnFileMRUFile) EVT_MENU(wxID_UNDO, GameFrame::OnEditUndo) EVT_MENU(wxID_REDO, GameFrame::OnEditRedo) EVT_MENU(GBT_MENU_EDIT_GAME, GameFrame::OnEditGame) +EVT_MENU(wxID_PREFERENCES, GameFrame::OnPrefs) EVT_MENU(GBT_MENU_VIEW_PROFILES, GameFrame::OnViewProfiles) EVT_MENU(GBT_MENU_VIEW_ZOOMIN, GameFrame::OnViewZoom) EVT_MENU(GBT_MENU_VIEW_ZOOMOUT, GameFrame::OnViewZoom) @@ -335,7 +337,7 @@ void GameFrame::OnUpdate() } if (m_doc->IsModified()) { - SetTitle(GetTitle() + wxT(" (unsaved changes)")); + SetTitle(GetTitle() + _(" (unsaved changes)")); } wxMenuBar *menuBar = GetMenuBar(); @@ -381,6 +383,12 @@ const wxSize TOOLBAR_ICON_SIZE(24, 24); void GameFrame::MakeMenus() { + // A re-entrant call (e.g. language switch) must unregister the outgoing File + // menu from the MRU history before SetMenuBar() destroys it. + if (wxMenuBar *oldBar = GetMenuBar()) { + wxGetApp().RemoveMenu(oldBar->GetMenu(0)); + } + auto *fileMenu = new wxMenu; auto *fileNewMenu = new wxMenu; @@ -425,6 +433,8 @@ void GameFrame::MakeMenus() editMenu->Append(wxID_REDO, _("&Redo\tShift-Ctrl-Z"), _("Redo the last undone change")); editMenu->AppendSeparator(); editMenu->Append(GBT_MENU_EDIT_GAME, _("&Game"), _("Edit properties of the game")); + editMenu->AppendSeparator(); + editMenu->Append(wxID_PREFERENCES, _("&Preferences"), _("Set preferences")); auto *viewMenu = new wxMenu; viewMenu->Append(GBT_MENU_VIEW_PROFILES, _("&Profiles"), _("Display/hide profiles window"), @@ -444,7 +454,7 @@ void GameFrame::MakeMenus() viewMenu->AppendSeparator(); viewMenu->Append(GBT_MENU_VIEW_STRATEGIC, _("&Strategic game"), - wxT("Display the reduced strategic representation ") wxT("of the game"), true); + _("Display the reduced strategic representation of the game"), true); if (!m_doc->GetGame()->IsTree()) { viewMenu->Check(GBT_MENU_VIEW_STRATEGIC, true); viewMenu->Enable(GBT_MENU_VIEW_STRATEGIC, false); @@ -485,7 +495,15 @@ void GameFrame::MakeMenus() void GameFrame::MakeToolbar() { - wxToolBar *toolBar = CreateToolBar(wxTB_HORIZONTAL | wxTB_FLAT); + // A re-entrant call (e.g. a language switch) must not destroy and + // recreate the toolbar: Destroy() is deferred, so CreateToolBar() would reuse + // a native HWND that is still pending deletion, crashing on the second call. + // Instead, reuse the existing toolbar and rebuild its contents in place. + wxToolBar *toolBar = GetToolBar(); + if (toolBar == nullptr) { + toolBar = CreateToolBar(wxTB_HORIZONTAL | wxTB_FLAT); + } + toolBar->ClearTools(); toolBar->SetMargins(2, 2); toolBar->SetToolBitmapSize(wxSize(24, 24)); @@ -533,6 +551,15 @@ void GameFrame::MakeToolbar() toolBar->SetRows(1); } +void GameFrame::Retranslate() +{ + MakeMenus(); + MakeToolbar(); + OnUpdate(); + Layout(); + Refresh(); +} + //---------------------------------------------------------------------- // GameFrame: Menu handlers - File menu //---------------------------------------------------------------------- @@ -778,10 +805,10 @@ void GameFrame::OnFileExportPS(wxCommandEvent &) wxPostScriptDC dc(printData); dc.SetBackgroundMode(wxTRANSPARENT); if (m_efgPanel && m_efgPanel->IsShown()) { - dc.StartDoc(_T("Gambit extensive game")); + dc.StartDoc(_("Gambit extensive game")); } else { - dc.StartDoc(_T("Gambit strategic game")); + dc.StartDoc(_("Gambit strategic game")); } dc.StartPage(); if (m_efgPanel && m_efgPanel->IsShown()) { @@ -939,20 +966,22 @@ void GameFrame::OnViewStrategic(wxCommandEvent &p_event) if (m_efgPanel->IsShown()) { // We are switching to strategic view if (!m_doc->GetGame()->IsPerfectRecall()) { - wxMessageBox(wxT("This is not a game of perfect recall.\n") - wxT("Computing the reduced strategic representation ") - wxT("of this game is not supported."), - wxT("Show strategic game"), wxOK); + wxMessageBox(_("This is not a game of perfect recall.\n" + "Computing the reduced strategic representation " + "of this game is not supported."), + _("Show strategic game"), wxOK); return; } if (const size_t contingencies = m_doc->GetGame()->GetStrategies().extent_product(); !m_nfgPanel && contingencies >= 50000) { wxString msg; - msg << "This game has " << contingencies << " contingencies in strategic form.\n" - << "Performance in browsing strategic form will be poor,\n" - << "and may render the program nonresponsive.\n" - << "Do you wish to continue?"; + msg = wxString::Format( + _("This game has %llu contingencies in strategic form.\n" + "Performance in browsing strategic form will be poor,\n" + "and may render the program nonresponsive.\n" + "Do you wish to continue?"), + static_cast(contingencies)); if (wxMessageBox(msg, _("Large strategic game warning"), wxOK | wxCANCEL | wxICON_WARNING, this) != wxOK) { @@ -1024,6 +1053,16 @@ void GameFrame::OnFormatLabels(wxCommandEvent &) } } +void GameFrame::OnPrefs(wxCommandEvent &) +{ + PreferencesDialog dialog(this); + if (dialog.ShowModal() != wxID_OK) { + return; + } + // Apply the new language (rebuilds menus/toolbar/labels). + Retranslate(); +} + void GameFrame::OnFormatFonts(wxCommandEvent &) { wxFontData data; @@ -1057,9 +1096,9 @@ extern void ShowNashMonitorDialog(wxWindow *p_parent, const std::shared_ptrGetGame()->IsPerfectRecall()) { - wxMessageBox(wxT("This is not a game of perfect recall.\n") - wxT("Computing Nash equilibria of this game ") wxT("is not supported."), - wxT("Show strategic game"), wxOK); + wxMessageBox(_("This is not a game of perfect recall.\n" + "Computing Nash equilibria of this game is not supported."), + _("Show strategic game"), wxOK); return; } @@ -1070,10 +1109,12 @@ void GameFrame::OnToolsEquilibrium(wxCommandEvent &) if (const int contingencies = m_doc->GetGame()->GetStrategies().extent_product(); contingencies >= 50000) { wxString msg; - msg << "This game has " << contingencies << " contingencies in strategic form.\n" - << "Performance in solving strategic form will be poor,\n" - << "and may render the program nonresponsive.\n" - << "Do you wish to continue?"; + msg = wxString::Format( + _("This game has %d contingencies in strategic form.\n" + "Performance in solving strategic form will be poor,\n" + "and may render the program nonresponsive.\n" + "Do you wish to continue?"), + contingencies); if (wxMessageBox(msg, _("Large strategic game warning"), wxOK | wxCANCEL | wxALIGN_CENTER, this) != wxOK) { @@ -1100,10 +1141,9 @@ void GameFrame::OnToolsEquilibrium(wxCommandEvent &) void GameFrame::OnToolsQre(wxCommandEvent &) { if (!m_doc->GetGame()->IsPerfectRecall()) { - wxMessageBox(wxT("This is not a game of perfect recall.\n") - wxT("Computing quantal response equilibria of this game ") - wxT("is not supported."), - wxT("Show strategic game"), wxOK); + wxMessageBox(_("This is not a game of perfect recall.\n" + "Computing quantal response equilibria of this game is not supported."), + _("Show strategic game"), wxOK); return; } diff --git a/src/gui/gameframe.h b/src/gui/gameframe.h index e18e0ed23..a54f0849c 100644 --- a/src/gui/gameframe.h +++ b/src/gui/gameframe.h @@ -70,6 +70,7 @@ class GameFrame final : public wxFrame, public GameView { void OnEditRedo(wxCommandEvent &); void OnEditGame(wxCommandEvent &); + void OnPrefs(wxCommandEvent &); void OnViewProfiles(wxCommandEvent &); void OnViewZoom(wxCommandEvent &); @@ -96,6 +97,9 @@ class GameFrame final : public wxFrame, public GameView { // Overriding GameView members void OnUpdate() override; + //! Rebuilds the menus, toolbar and window chrome after a language change. + void Retranslate(); + public: // CONSTRUCTOR AND DESTRUCTOR GameFrame(wxWindow *p_parent, const std::shared_ptr &p_doc); diff --git a/src/gui/nfgheadergrid.cc b/src/gui/nfgheadergrid.cc index 7f324bbca..736f326e3 100644 --- a/src/gui/nfgheadergrid.cc +++ b/src/gui/nfgheadergrid.cc @@ -116,7 +116,7 @@ class RowPlayerTable final : public wxGridTableBase { wxString RowPlayerTable::GetValue(int row, int col) { if (m_table->GetRowHeaderColCount() == 0) { - return wxT("Payoffs"); + return _("Payoffs"); } const int player = m_table->GetRowHeaderPlayer(col); @@ -653,7 +653,7 @@ class ColPlayerTable final : public wxGridTableBase { wxString ColPlayerTable::GetValue(int row, int col) { if (m_table->GetColHeaderRowCount() == 0) { - return wxT("Payoffs"); + return _("Payoffs"); } const int player = m_table->GetColHeaderPlayer(row); diff --git a/src/gui/nfgpanel.cc b/src/gui/nfgpanel.cc index 65f2263aa..58221e305 100644 --- a/src/gui/nfgpanel.cc +++ b/src/gui/nfgpanel.cc @@ -68,17 +68,17 @@ StrategyDominanceToolbar::StrategyDominanceToolbar(wxWindow *p_parent, { auto *topSizer = new wxBoxSizer(wxHORIZONTAL); - topSizer->Add(new wxStaticText(this, wxID_STATIC, wxT("Hide strategies which are ")), 0, + topSizer->Add(new wxStaticText(this, wxID_STATIC, _("Hide strategies which are ")), 0, wxALL | wxALIGN_CENTER, 5); - wxString domChoices[] = {wxT("strictly"), wxT("strictly or weakly")}; + wxString domChoices[] = {_("strictly"), _("strictly or weakly")}; auto *choice = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 2, domChoices); choice->SetSelection(0); Connect(choice->GetId(), wxEVT_COMMAND_CHOICE_SELECTED, wxCommandEventHandler(StrategyDominanceToolbar::OnStrength)); topSizer->Add(choice, 0, wxALL | wxALIGN_CENTER, 5); - topSizer->Add(new wxStaticText(this, wxID_STATIC, wxT("dominated:")), 0, wxALL | wxALIGN_CENTER, + topSizer->Add(new wxStaticText(this, wxID_STATIC, _("dominated:")), 0, wxALL | wxALIGN_CENTER, 5); m_topButton = @@ -95,7 +95,7 @@ StrategyDominanceToolbar::StrategyDominanceToolbar(wxWindow *p_parent, wxCommandEventHandler(StrategyDominanceToolbar::OnPreviousLevel)); topSizer->Add(m_prevButton, 0, wxALL | wxALIGN_CENTER, 5); - m_level = new wxStaticText(this, wxID_STATIC, wxT("All strategies shown"), wxDefaultPosition, + m_level = new wxStaticText(this, wxID_STATIC, _("All strategies shown"), wxDefaultPosition, wxDefaultSize, wxALIGN_CENTER | wxST_NO_AUTORESIZE); topSizer->Add(m_level, 0, wxALL | wxALIGN_CENTER, 5); @@ -144,15 +144,16 @@ void StrategyDominanceToolbar::OnUpdate() m_nextButton->Enable(m_doc->GetWorkspace().CanStrategyElim()); m_allButton->Enable(m_doc->GetWorkspace().CanStrategyElim()); if (m_doc->GetWorkspace().GetStrategyElimLevel() == 1) { - m_level->SetLabel(wxT("All strategies shown")); + m_level->SetLabel(_("All strategies shown")); } else if (m_doc->GetWorkspace().GetStrategyElimLevel() == 2) { - m_level->SetLabel(wxT("Eliminated 1 level")); + m_level->SetLabel(_("Eliminated 1 level")); } else { - wxString label; - label << "Eliminated " << (m_doc->GetWorkspace().GetStrategyElimLevel() - 1) << " levels"; - m_level->SetLabel(label); + m_level->SetLabel(wxString::Format( + wxPLURAL("Eliminated %d level", "Eliminated %d levels", + m_doc->GetWorkspace().GetStrategyElimLevel() - 1), + m_doc->GetWorkspace().GetStrategyElimLevel() - 1)); } GetSizer()->Layout(); } diff --git a/src/gui/valnumber.cc b/src/gui/valnumber.cc index 27c23203a..856713569 100644 --- a/src/gui/valnumber.cc +++ b/src/gui/valnumber.cc @@ -114,8 +114,8 @@ bool NumberValidator::Validate(wxWindow *p_parent) const wxString value(control->GetValue()); if (!IsNumeric(value)) { - wxMessageBox(_T("The value ") + value + _T(" in ") + m_validatorWindow->GetName() + - _T(" is not a valid number."), + wxMessageBox(wxString::Format(_("The value %s in %s is not a valid number."), value, + m_validatorWindow->GetName()), _("Error"), wxOK | wxICON_EXCLAMATION, p_parent); m_validatorWindow->SetFocus(); return false; @@ -124,10 +124,10 @@ bool NumberValidator::Validate(wxWindow *p_parent) if ((m_hasMin && lexical_cast(std::string(value.mb_str())) < m_minValue) || (m_hasMax && lexical_cast(std::string(value.mb_str())) > m_maxValue)) { wxMessageBox( - _T("The value ") + value + _T(" in ") + m_validatorWindow->GetName() + - _T(" is out of the range [") + - wxString(lexical_cast(m_minValue).c_str(), *wxConvCurrent) + _T(", ") + - wxString(lexical_cast(m_maxValue).c_str(), *wxConvCurrent) + _T("]."), + wxString::Format(_("The value %s in %s is out of the range [%s, %s]."), value, + m_validatorWindow->GetName(), + wxString(lexical_cast(m_minValue).c_str(), *wxConvCurrent), + wxString(lexical_cast(m_maxValue).c_str(), *wxConvCurrent)), _("Error"), wxOK | wxICON_EXCLAMATION, p_parent); m_validatorWindow->SetFocus(); return false; diff --git a/src/gui/welcome.cc b/src/gui/welcome.cc index 1ea42f35d..8044b1cba 100644 --- a/src/gui/welcome.cc +++ b/src/gui/welcome.cc @@ -68,22 +68,22 @@ void WelcomePanel::CreateControls() { m_logoBitmap = new wxStaticBitmap(this, wxID_ANY, MakeScaledBitmap(gambitbig_xpm, 72)); - m_titleText = new wxStaticText(this, wxID_ANY, "Welcome to Gambit"); + m_titleText = new wxStaticText(this, wxID_ANY, _("Welcome to Gambit")); auto titleFont = m_titleText->GetFont(); titleFont.SetPointSize(titleFont.GetPointSize() + 6); titleFont.SetWeight(wxFONTWEIGHT_BOLD); m_titleText->SetFont(titleFont); - m_openButton = new wxCommandLinkButton(this, ID_WELCOME_OPEN, "Open existing game", - "Load a saved .efg or .nfg file"); + m_openButton = new wxCommandLinkButton(this, ID_WELCOME_OPEN, _("Open existing game"), + _("Load a saved .efg or .nfg file")); m_newNormalFormButton = - new wxCommandLinkButton(this, ID_WELCOME_NEW_NORMAL_FORM, "New strategic form game", - "Create a game with a strategic representation"); + new wxCommandLinkButton(this, ID_WELCOME_NEW_NORMAL_FORM, _("New strategic form game"), + _("Create a game with a strategic representation")); m_newExtensiveFormButton = - new wxCommandLinkButton(this, ID_WELCOME_NEW_EXTENSIVE_FORM, "New extensive form game", - "Create a game with a tree representation"); + new wxCommandLinkButton(this, ID_WELCOME_NEW_EXTENSIVE_FORM, _("New extensive form game"), + _("Create a game with a tree representation")); m_openButton->SetBitmap(wxBitmapBundle::FromSVG(open_svg, wxSize(24, 24))); m_openButton->SetBitmapMargins(24, 12);