From 496f3eb577739bf667665efc490e583baa45eb2e Mon Sep 17 00:00:00 2001
From: David Robillard <d@drobilla.net>
Date: Tue, 23 Aug 2022 00:52:14 -0400
Subject: Add i18n support

---
 .reuse/dep5                    |   4 ++
 NEWS                           |   3 +-
 meson.build                    |  16 +++++
 meson/suppressions/meson.build |   1 +
 po/LINGUAS                     |   2 +
 po/POTFILES                    |  60 ++++++++++++++++
 po/meson.build                 |  27 +++++++
 po/patchage.pot                | 155 +++++++++++++++++++++++++++++++++++++++++
 src/CanvasModule.cpp           |   6 +-
 src/CanvasPort.hpp             |   3 +-
 src/Legend.cpp                 |   3 +-
 src/Patchage.cpp               |  37 ++++++----
 src/i18n.hpp                   |  15 ++++
 src/main.cpp                   |  11 +++
 src/patchage.ui.in             |   6 +-
 src/patchage_config.h          |  19 +++++
 16 files changed, 344 insertions(+), 24 deletions(-)
 create mode 100644 po/LINGUAS
 create mode 100644 po/POTFILES
 create mode 100644 po/meson.build
 create mode 100644 po/patchage.pot
 create mode 100644 src/i18n.hpp

diff --git a/.reuse/dep5 b/.reuse/dep5
index 7749513..5bfac99 100644
--- a/.reuse/dep5
+++ b/.reuse/dep5
@@ -30,3 +30,7 @@ License: CC-BY-SA-4.0 OR GPL-3.0-or-later
 Files: icons/*.png icons/*.svg
 Copyright: 2007-2011 David Robillard <d@drobilla.net>
 License: CC-BY-SA-4.0 OR GPL-3.0-or-later
+
+Files: po/patchage.pot
+Copyright: 2022 David Robillard <d@drobilla.net>
+License: GPL-3.0-or-later
diff --git a/NEWS b/NEWS
index 7cb305f..208dbec 100644
--- a/NEWS
+++ b/NEWS
@@ -1,9 +1,10 @@
 patchage (1.0.9) unstable; urgency=medium
 
+  * Add i18n support
   * Replace boost with standard C++17 facilities
   * Upgrade to fmt 9.0.0
 
- -- David Robillard <d@drobilla.net>  Mon, 22 Aug 2022 17:07:17 +0000
+ -- David Robillard <d@drobilla.net>  Tue, 23 Aug 2022 02:49:17 +0000
 
 patchage (1.0.8) stable; urgency=medium
 
diff --git a/meson.build b/meson.build
index b49a56b..e5a5969 100644
--- a/meson.build
+++ b/meson.build
@@ -37,10 +37,12 @@ add_project_arguments(['-DFMT_HEADER_ONLY'], language: ['cpp'])
 ##########################
 
 patchage_datadir = get_option('prefix') / get_option('datadir') / 'patchage'
+patchage_localedir = get_option('prefix') / get_option('localedir')
 
 platform_defines = [
   '-DPATCHAGE_VERSION="@0@"'.format(meson.project_version()),
   '-DPATCHAGE_DATA_DIR="@0@"'.format(patchage_datadir),
+  '-DPATCHAGE_LOCALE_DIR="@0@"'.format(patchage_localedir),
 ]
 
 if host_machine.system() in ['gnu', 'linux']
@@ -53,6 +55,9 @@ if get_option('checks')
   dladdr_code = '''#include <dlfcn.h>
 int main(void) { Dl_info info; return dladdr(&info, &info); }'''
 
+  gettext_code = '''#include <libintl.h>
+int main(void) { return !!gettext("hello"); }'''
+
   jack_metadata_code = '''#include <jack/metadata.h>
 int main(void) { return !!&jack_set_property; }'''
 
@@ -65,6 +70,11 @@ int main(void) { return !!&jack_set_property; }'''
     platform_defines += ['-DHAVE_DLADDR=0']
   endif
 
+  platform_defines += '-DHAVE_GETTEXT=@0@'.format(
+    cpp.compiles(gettext_code,
+                 args: platform_defines,
+                 name: 'gettext').to_int())
+
   platform_defines += '-DHAVE_JACK_METADATA=@0@'.format(
     cpp.compiles(jack_metadata_code,
                  args: platform_defines,
@@ -160,6 +170,12 @@ if jack_dep.found() and dbus_dep.found() and dbus_glib_dep.found()
   message('Both libjack and D-Bus available, defaulting to libjack')
 endif
 
+#######################
+# Translations (i18n) #
+#######################
+
+subdir('po')
+
 ###########
 # Program #
 ###########
diff --git a/meson/suppressions/meson.build b/meson/suppressions/meson.build
index 8292d77..0875ccd 100644
--- a/meson/suppressions/meson.build
+++ b/meson/suppressions/meson.build
@@ -18,6 +18,7 @@ if is_variable('cpp')
     if cpp.get_id() == 'clang'
       cpp_suppressions += [
         '-Wno-alloca',
+        '-Wno-c++20-compat',
         '-Wno-cast-qual',
         '-Wno-double-promotion',
         '-Wno-float-conversion',
diff --git a/po/LINGUAS b/po/LINGUAS
new file mode 100644
index 0000000..ebf3dcb
--- /dev/null
+++ b/po/LINGUAS
@@ -0,0 +1,2 @@
+# Copyright 2022 David Robillard <d@drobilla.net>
+# SPDX-License-Identifier: CC0-1.0 OR GPL-3.0-or-later
diff --git a/po/POTFILES b/po/POTFILES
new file mode 100644
index 0000000..0f392b7
--- /dev/null
+++ b/po/POTFILES
@@ -0,0 +1,60 @@
+# Copyright 2022 David Robillard <d@drobilla.net>
+# SPDX-License-Identifier: CC0-1.0 OR GPL-3.0-or-later
+
+src/Action.hpp
+src/ActionSink.hpp
+src/AlsaDriver.cpp
+src/AlsaStubDriver.cpp
+src/AudioDriver.hpp
+src/Canvas.cpp
+src/Canvas.hpp
+src/CanvasModule.cpp
+src/CanvasModule.hpp
+src/CanvasPort.hpp
+src/ClientID.hpp
+src/ClientInfo.hpp
+src/ClientType.hpp
+src/Configuration.cpp
+src/Configuration.hpp
+src/Coord.hpp
+src/Driver.hpp
+src/Drivers.cpp
+src/Drivers.hpp
+src/Event.hpp
+src/ILog.hpp
+src/JackDbusDriver.cpp
+src/JackLibDriver.cpp
+src/JackStubDriver.cpp
+src/Legend.cpp
+src/Legend.hpp
+src/Metadata.cpp
+src/Metadata.hpp
+src/Options.hpp
+src/Patchage.cpp
+src/Patchage.hpp
+src/PortID.hpp
+src/PortInfo.hpp
+src/PortNames.hpp
+src/PortType.hpp
+src/Reactor.cpp
+src/Reactor.hpp
+src/Setting.hpp
+src/SignalDirection.hpp
+src/TextViewLog.cpp
+src/TextViewLog.hpp
+src/UIFile.hpp
+src/Widget.hpp
+src/binary_location.h
+src/event_to_string.cpp
+src/event_to_string.hpp
+src/handle_event.cpp
+src/handle_event.hpp
+src/jackey.h
+src/main.cpp
+src/make_alsa_driver.hpp
+src/make_jack_driver.hpp
+# src/patchage.gladep
+# src/patchage.svg
+src/patchage.ui.in
+src/patchage_config.h
+src/warnings.hpp
diff --git a/po/meson.build b/po/meson.build
new file mode 100644
index 0000000..689ed1b
--- /dev/null
+++ b/po/meson.build
@@ -0,0 +1,27 @@
+# Copyright 2020-2022 David Robillard <d@drobilla.net>
+# SPDX-License-Identifier: CC0-1.0 OR GPL-3.0-or-later
+
+i18n = import('i18n')
+
+add_project_arguments(
+  ['-DGETTEXT_PACKAGE="@0@"'.format(meson.project_name())],
+  language: 'cpp',
+)
+
+i18n.gettext(
+  meson.project_name(),
+  args: [
+    '--add-comments',
+    '--check=bullet-unicode',
+    '--check=ellipsis-unicode',
+    '--check=quote-unicode',
+    '--check=space-ellipsis',
+    '--copyright-holder=FULL NAME <EMAIL@ADDRESS>',
+    '--from-code=UTF-8',
+    '--msgid-bugs-address=https://gitlab.com/drobilla/patchage/issues/new',
+    '--package-version=@0@'.format(meson.project_version()),
+    '--sentence-end=double-space',
+    '--sort-by-file',
+    '--width=80',
+  ],
+)
diff --git a/po/patchage.pot b/po/patchage.pot
new file mode 100644
index 0000000..e12cf8f
--- /dev/null
+++ b/po/patchage.pot
@@ -0,0 +1,155 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR FULL NAME <EMAIL@ADDRESS>
+# This file is distributed under the same license as the patchage package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: patchage 1.0.9\n"
+"Report-Msgid-Bugs-To: https://gitlab.com/drobilla/patchage/issues/new\n"
+"POT-Creation-Date: 2022-08-23 00:52-0400\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: src/CanvasModule.cpp:93
+msgid "_Split"
+msgstr ""
+
+#: src/CanvasModule.cpp:97
+msgid "_Join"
+msgstr ""
+
+#: src/CanvasModule.cpp:101
+msgid "_Disconnect"
+msgstr ""
+
+#: src/CanvasPort.hpp:86
+msgid "Disconnect"
+msgstr ""
+
+#: src/Legend.cpp:27
+msgid "Audio"
+msgstr ""
+
+#: src/Patchage.cpp:453
+msgid "frames at {} kHz ({:0.2f} ms)"
+msgstr ""
+
+#: src/Patchage.cpp:475 src/Patchage.cpp:510 src/patchage.ui.in:391
+msgid "Dropouts: {}"
+msgstr ""
+
+#: src/Patchage.cpp:838
+msgid "Export Image"
+msgstr ""
+
+#: src/Patchage.cpp:860
+msgid "Draw _Background"
+msgstr ""
+
+#: src/Patchage.cpp:871
+msgid "File exists!  Overwrite {}?"
+msgstr ""
+
+#: src/patchage.ui.in:21
+msgid "_File"
+msgstr ""
+
+#: src/patchage.ui.in:30
+msgid "_Export Image…"
+msgstr ""
+
+#: src/patchage.ui.in:60
+msgid "_System"
+msgstr ""
+
+#: src/patchage.ui.in:67
+msgid "Connect to _JACK"
+msgstr ""
+
+#: src/patchage.ui.in:78
+msgid "Disconnect from JACK"
+msgstr ""
+
+#: src/patchage.ui.in:95
+msgid "Connect to _ALSA"
+msgstr ""
+
+#: src/patchage.ui.in:106
+msgid "Disconnect from ALSA"
+msgstr ""
+
+#: src/patchage.ui.in:123
+msgid "_View"
+msgstr ""
+
+#: src/patchage.ui.in:132
+msgid "_Messages"
+msgstr ""
+
+#: src/patchage.ui.in:141
+msgid "Tool_bar"
+msgstr ""
+
+#: src/patchage.ui.in:157
+msgid "_Human Names"
+msgstr ""
+
+#: src/patchage.ui.in:167
+msgid "_Sort Ports by Name"
+msgstr ""
+
+#: src/patchage.ui.in:230
+msgid "_Increase Font Size"
+msgstr ""
+
+#: src/patchage.ui.in:239
+msgid "_Decrease Font Size"
+msgstr ""
+
+#: src/patchage.ui.in:248
+msgid "_Normal Font Size"
+msgstr ""
+
+#: src/patchage.ui.in:272
+msgid "_Arrange"
+msgstr ""
+
+#: src/patchage.ui.in:285
+msgid "Sprung Layou_t"
+msgstr ""
+
+#: src/patchage.ui.in:298
+msgid "_Help"
+msgstr ""
+
+#: src/patchage.ui.in:341
+msgid "JACK buffer size and sample rate."
+msgstr ""
+
+#: src/patchage.ui.in:353
+msgid "JACK buffer length in frames."
+msgstr ""
+
+#: src/patchage.ui.in:365
+msgid "frames at ? kHz (? ms)"
+msgstr ""
+
+#: src/patchage.ui.in:405
+msgid "Clear dropout indicator."
+msgstr ""
+
+#: src/patchage.ui.in:505
+msgid "A modular patchbay for JACK and ALSA applications."
+msgstr ""
+
+#. TRANSLATORS: Replace this string with your names, one name per line.
+#: src/patchage.ui.in:1183
+msgid "translator-credits"
+msgstr ""
diff --git a/src/CanvasModule.cpp b/src/CanvasModule.cpp
index d3908df..e93cefe 100644
--- a/src/CanvasModule.cpp
+++ b/src/CanvasModule.cpp
@@ -90,15 +90,15 @@ CanvasModule::show_menu(GdkEventButton* ev)
 
   if (_type == SignalDirection::duplex) {
     items.push_back(Gtk::Menu_Helpers::MenuElem(
-      "_Split", sigc::mem_fun(this, &CanvasModule::on_split)));
+      _("_Split"), sigc::mem_fun(this, &CanvasModule::on_split)));
     update_menu();
   } else {
     items.push_back(Gtk::Menu_Helpers::MenuElem(
-      "_Join", sigc::mem_fun(this, &CanvasModule::on_join)));
+      _("_Join"), sigc::mem_fun(this, &CanvasModule::on_join)));
   }
 
   items.push_back(Gtk::Menu_Helpers::MenuElem(
-    "_Disconnect", sigc::mem_fun(this, &CanvasModule::on_disconnect)));
+    _("_Disconnect"), sigc::mem_fun(this, &CanvasModule::on_disconnect)));
 
   _menu->popup(ev->button, ev->time);
   return true;
diff --git a/src/CanvasPort.hpp b/src/CanvasPort.hpp
index 548c9e5..c3195a9 100644
--- a/src/CanvasPort.hpp
+++ b/src/CanvasPort.hpp
@@ -6,6 +6,7 @@
 
 #include "PortID.hpp"
 #include "PortType.hpp"
+#include "i18n.hpp"
 #include "warnings.hpp"
 
 PATCHAGE_DISABLE_GANV_WARNINGS
@@ -82,7 +83,7 @@ public:
 
     Gtk::Menu* menu = Gtk::manage(new Gtk::Menu());
     menu->items().push_back(Gtk::Menu_Helpers::MenuElem(
-      "Disconnect", sigc::mem_fun(this, &Port::disconnect)));
+      _("Disconnect"), sigc::mem_fun(this, &Port::disconnect)));
 
     menu->popup(ev->button.button, ev->button.time);
     return true;
diff --git a/src/Legend.cpp b/src/Legend.cpp
index bdf51bc..a4e8705 100644
--- a/src/Legend.cpp
+++ b/src/Legend.cpp
@@ -5,6 +5,7 @@
 
 #include "Configuration.hpp"
 #include "PortType.hpp"
+#include "i18n.hpp"
 #include "patchage_config.h"
 
 #include <gdkmm/color.h>
@@ -23,7 +24,7 @@ namespace patchage {
 Legend::Legend(const Configuration& configuration)
 {
   add_button(PortType::jack_audio,
-             "Audio",
+             _("Audio"),
              configuration.get_port_color(PortType::jack_audio));
 
 #if USE_JACK_METADATA
diff --git a/src/Patchage.cpp b/src/Patchage.cpp
index 1f3d706..c3166c2 100644
--- a/src/Patchage.cpp
+++ b/src/Patchage.cpp
@@ -23,6 +23,7 @@
 #include "Widget.hpp"
 #include "event_to_string.hpp"
 #include "handle_event.hpp"
+#include "i18n.hpp"
 #include "patchage_config.h" // IWYU pragma: keep
 #include "warnings.hpp"
 
@@ -445,11 +446,14 @@ Patchage::update_toolbar()
     const auto buffer_size = _drivers.jack()->buffer_size();
     const auto sample_rate = _drivers.jack()->sample_rate();
     if (sample_rate != 0) {
-      const auto latency_ms =
-        buffer_size * 1000 / static_cast<float>(sample_rate);
+      const auto sample_rate_khz = sample_rate / 1000.0;
+      const auto latency_ms      = buffer_size / sample_rate_khz;
+
+      _latency_label->set_label(" " +
+                                fmt::format(_("frames at {} kHz ({:0.2f} ms)"),
+                                            sample_rate_khz,
+                                            latency_ms));
 
-      _latency_label->set_label(fmt::format(
-        " frames @ {} kHz ({:0.2f} ms)", sample_rate / 1000, latency_ms));
       _latency_label->set_visible(true);
       _buf_size_combo->set_active(
         static_cast<int>(log2f(_drivers.jack()->buffer_size()) - 5));
@@ -467,12 +471,13 @@ Patchage::update_load()
 {
   if (_drivers.jack() && _drivers.jack()->is_attached()) {
     const auto xruns = _drivers.jack()->xruns();
+
+    _dropouts_label->set_text(" " + fmt::format(_("Dropouts: {}"), xruns));
+
     if (xruns > 0u) {
-      _dropouts_label->set_text(fmt::format(" Dropouts: {}", xruns));
       _dropouts_label->show();
       _clear_load_but->show();
     } else {
-      _dropouts_label->set_text(" Dropouts: 0");
       _dropouts_label->hide();
       _clear_load_but->hide();
     }
@@ -502,7 +507,7 @@ Patchage::store_window_location()
 void
 Patchage::clear_load()
 {
-  _dropouts_label->set_text(" Dropouts: 0");
+  _dropouts_label->set_text(" " + fmt::format(_("Dropouts: {}"), 0U));
   _dropouts_label->hide();
   _clear_load_but->hide();
   if (_drivers.jack()) {
@@ -830,7 +835,9 @@ Patchage::on_quit()
 void
 Patchage::on_export_image()
 {
-  Gtk::FileChooserDialog dialog("Export Image", Gtk::FILE_CHOOSER_ACTION_SAVE);
+  Gtk::FileChooserDialog dialog(_("Export Image"),
+                                Gtk::FILE_CHOOSER_ACTION_SAVE);
+
   dialog.add_button(Gtk::Stock::CANCEL, Gtk::RESPONSE_CANCEL);
   dialog.add_button(Gtk::Stock::SAVE, Gtk::RESPONSE_OK);
   dialog.set_default_response(Gtk::RESPONSE_OK);
@@ -850,7 +857,7 @@ Patchage::on_export_image()
     dialog.add_filter(filt);
   }
 
-  auto* bg_but = new Gtk::CheckButton("Draw _Background", true);
+  auto* bg_but = new Gtk::CheckButton(_("Draw _Background"), true);
   auto* extra  = new Gtk::Alignment(1.0, 0.5, 0.0, 0.0);
   bg_but->set_active(true);
   extra->add(*Gtk::manage(bg_but));
@@ -860,12 +867,12 @@ Patchage::on_export_image()
   if (dialog.run() == Gtk::RESPONSE_OK) {
     const std::string filename = dialog.get_filename();
     if (Glib::file_test(filename, Glib::FILE_TEST_EXISTS)) {
-      Gtk::MessageDialog confirm(std::string("File exists!  Overwrite ") +
-                                   filename + "?",
-                                 true,
-                                 Gtk::MESSAGE_WARNING,
-                                 Gtk::BUTTONS_YES_NO,
-                                 true);
+      Gtk::MessageDialog confirm(
+        fmt::format(_("File exists!  Overwrite {}?"), filename),
+        true,
+        Gtk::MESSAGE_WARNING,
+        Gtk::BUTTONS_YES_NO,
+        true);
       confirm.set_transient_for(dialog);
       if (confirm.run() != Gtk::RESPONSE_YES) {
         return;
diff --git a/src/i18n.hpp b/src/i18n.hpp
new file mode 100644
index 0000000..4cf082d
--- /dev/null
+++ b/src/i18n.hpp
@@ -0,0 +1,15 @@
+// Copyright 2022 David Robillard <d@drobilla.net>
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#ifndef PATCHAGE_I18N_HPP
+#define PATCHAGE_I18N_HPP
+
+#include <libintl.h>
+
+/// Mark a string literal as translatable
+#define _(msgid) gettext(msgid)
+
+/// Mark a string literal as non-translatable
+// #define N_(msgid) (msgid)
+
+#endif // PATCHAGE_I18N_HPP
diff --git a/src/main.cpp b/src/main.cpp
index 4466e15..d76413f 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -21,6 +21,10 @@
 #include <glibmm/ustring.h>
 #include <gtkmm/main.h>
 
+#if USE_GETTEXT
+#  include <libintl.h>
+#endif
+
 #include <cstring>
 #include <exception>
 #include <iostream>
@@ -94,6 +98,13 @@ main(int argc, char** argv)
   set_bundle_environment();
 #endif
 
+#if USE_GETTEXT
+  setlocale(LC_ALL, "");
+  bindtextdomain("patchage", PATCHAGE_LOCALE_DIR);
+  bind_textdomain_codeset("patchage", "UTF-8");
+  textdomain("patchage");
+#endif
+
   try {
     Glib::thread_init();
 
diff --git a/src/patchage.ui.in b/src/patchage.ui.in
index c63566a..89d749d 100644
--- a/src/patchage.ui.in
+++ b/src/patchage.ui.in
@@ -362,7 +362,7 @@
                         <child>
                           <object class="GtkLabel" id="latency_label">
                             <property name="can_focus">False</property>
-                            <property name="label" translatable="yes">frames @ ? kHz (? ms)</property>
+                            <property name="label" translatable="yes">frames at ? kHz (? ms)</property>
                           </object>
                           <packing>
                             <property name="expand">False</property>
@@ -388,7 +388,7 @@
                   <object class="GtkLabel" id="dropouts_label">
                     <property name="can_focus">False</property>
                     <property name="visible">False</property>
-                    <property name="label" translatable="yes"> Dropouts: 0</property>
+                    <property name="label" translatable="yes">Dropouts: {}</property>
                   </object>
                 </child>
               </object>
@@ -502,7 +502,7 @@
     <property name="version">@PATCHAGE_VERSION@</property>
     <property name="copyright" translatable="no">© 2005-2022 David Robillard
 © 2008 Nedko Arnaudov</property>
-    <property name="comments" translatable="yes">A JACK and ALSA front-end.</property>
+    <property name="comments" translatable="yes">A modular patchbay for JACK and ALSA applications.</property>
     <property name="website">http://drobilla.net/software/patchage</property>
     <property name="license" translatable="no">                    GNU GENERAL PUBLIC LICENSE
                        Version 3, 29 June 2007
diff --git a/src/patchage_config.h b/src/patchage_config.h
index e5f56df..6c3ad90 100644
--- a/src/patchage_config.h
+++ b/src/patchage_config.h
@@ -46,6 +46,19 @@
 #    endif
 #  endif
 
+// GNU gettext()
+#  ifndef HAVE_GETTEXT
+#    ifdef __has_include
+#      if __has_include(<libintl.h>)
+#        define HAVE_GETTEXT 1
+#      else
+#        define HAVE_GETTEXT 0
+#      endif
+#    else
+#      define HAVE_GETTEXT 0
+#    endif
+#  endif
+
 // JACK metadata API
 #  ifndef HAVE_JACK_METADATA
 #    ifdef __has_include
@@ -75,6 +88,12 @@
 #  define USE_DLADDR 0
 #endif
 
+#if HAVE_GETTEXT
+#  define USE_GETTEXT 1
+#else
+#  define USE_GETTEXT 0
+#endif
+
 #if HAVE_JACK_METADATA
 #  define USE_JACK_METADATA 1
 #else
-- 
cgit v1.2.1