translations_po.cc 10.6 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* Glom
 *
 * Copyright (C) 2001-2012 Murray Cumming
 *
 * 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
17
18
 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301 USA.
19
20
21
22
23
24
 */

#include <libglom/translations_po.h>

// To read the .po files
#include <gettext-po.h>
25
#include "libglom/libglom_config.h" //For HAVE_GETTEXTPO_XERROR
26
27

#include <glibmm/convert.h>
28
29
#include <glibmm/fileutils.h>
#include <glibmm/datetime.h>
30
#include <glibmm/i18n-lib.h>
31
32
33
34

#include <iostream>

/* For really ugly hacks! */
35
#include <csetjmp>
36

37
38
39
40
#define GLOM_PO_HEADER \
"msgid \"\"\n" \
"msgstr \"\"\n" \
"\"Project-Id-Version: %1\\n\"\n" \
41
42
"\"Report-Msgid-Bugs-To: https://bugzilla.gnome.org/enter_bug.cgi?\"\n" \
"\"product=Glom&keywords=I18N+L10N&component=general\\n\"\n" \
43
44
45
46
47
48
49
"\"PO-Revision-Date: %2\\n\"\n" \
"\"Last-Translator: Someone <someone@someone.com>\\n\"\n" \
"\"Language-Team: %3 <someone@someone.com>\\n\"\n" \
"\"MIME-Version: 1.0\\n\"\n" \
"\"Content-Type: text/plain; charset=UTF-8\\n\"\n" \
"\"Content-Transfer-Encoding: 8bit\\n\""

50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
namespace Glom
{

static jmp_buf jump;

static void show_gettext_error(int severity, const char* filename, const gchar* message)
{
  std::ostringstream msg_stream;
  if(filename)
    msg_stream << filename << ": ";

  if(message)
   msg_stream << message;

  switch(severity)
  {
Murray Cumming's avatar
Murray Cumming committed
66
    #ifdef PO_SEVERITY_WARNING //This was introduced in libgettext-po some time after gettext version 0.14.5
67
68
69
70
71
72
73
74
75
    case PO_SEVERITY_WARNING:
    {
      // Show only debug output
      std::cout << _("Gettext-Warning: ") << msg_stream.str() << std::endl;
      break;
    }
    #endif //PO_SEVERITY_WARNING


Murray Cumming's avatar
Murray Cumming committed
76
    #ifdef PO_SEVERITY_ERROR //This was introduced in libgettext-po some time after gettext version 0.14.5
77
78
79
    case PO_SEVERITY_ERROR:
    #endif //PO_SEVERITY_ERROR

Murray Cumming's avatar
Murray Cumming committed
80
    #ifdef PO_SEVERITY_FATAL_ERROR //This was introduced in libgettext-po some time after gettext version 0.14.5
81
82
83
84
85
    case PO_SEVERITY_FATAL_ERROR:
    #endif //PO_SEVERITY_FATAL_ERROR

    default:
    {
Murray Cumming's avatar
Murray Cumming committed
86
      //TODO: const auto msg = Glib::ustring(_("Gettext-Error: ")) + ' ' + msg_stream.str();
87
88
89
90
      //Gtk::MessageDialog dlg(msg, false, Gtk::MESSAGE_ERROR);
      //dlg.run();
      break;
    }
Murray Cumming's avatar
Murray Cumming committed
91
  }
92
93
94
95
96
97
98
99
100
101
102
103
104
}

/*
 * The exception handling of libgettext-po is very ugly! The following methods are called
 * if an exception occurs and may not return in case of a fatal exception. We use setjmp
 * and longjmp to bypass this and return to the caller
 */
#ifdef HAVE_GETTEXTPO_XERROR
static void on_gettextpo_xerror (int severity, po_message_t /* message */, const char *filename, size_t /* lineno */, size_t /* column */,
  int /* multiline_p */, const char *message_text)
{
  show_gettext_error(severity, filename, message_text);

Murray Cumming's avatar
Murray Cumming committed
105
  #ifdef PO_SEVERITY_FATAL_ERROR  //This was introduced in libgettext-po some time after gettext version 0.14.5
106
107
108
109
110
111
112
113
114
115
116
  if(severity == PO_SEVERITY_FATAL_ERROR)
    longjmp(jump, 1);
  #endif //PO_SEVERITY_FATAL_ERROR
}

static void on_gettextpo_xerror2 (int severity, po_message_t /* message1 */, const char * filename1, size_t /* lineno1 */, size_t /* column1 */,
  int /* multiline_p1 */, const char *message_text1,
  po_message_t /* message2 */, const char * /*filename2 */, size_t /* lineno2 */, size_t /* column2 */,
  int /* multiline_p2 */, const char * /* message_text2 */)
{
  show_gettext_error(severity, filename1, message_text1);
Murray Cumming's avatar
Murray Cumming committed
117
118

  #ifdef PO_SEVERITY_FATAL_ERROR  //This was introduced in libgettext-po some time after gettext version 0.14.5
119
120
121
122
123
124
125
  if(severity == PO_SEVERITY_FATAL_ERROR)
    longjmp(jump, 1);
  #endif //PO_SEVERITY_FATAL_ERROR
}
#else //HAVE_GETTEXTPO_XERROR
static void on_gettextpo_error(int status, int errnum, const char * /* format */, ...)
{
126
  std::cerr << G_STRFUNC << ": gettext error (old libgettext-po API): status=" << status << ", errnum=" << errnum << std::endl;
127
128
129
}
#endif //HAVE_GETTEXTPO_XERROR

130
Glib::ustring get_po_context_for_item(const std::shared_ptr<const TranslatableItem>& item, const Glib::ustring& hint)
131
132
133
{
  // Note that this context string should use English rather than the translated strings,
  // or the context would change depending on the locale of the user doing the export:
134
135
  Glib::ustring result = TranslatableItem::get_translatable_type_name_nontranslated(item->get_translatable_item_type());

Murray Cumming's avatar
Murray Cumming committed
136
  const auto name = item->get_name();
137
138
139
140
141
142
  if(!name.empty())
    result += " (" + item->get_name() + ')';

  if(!hint.empty())
    result += ". " + hint;

143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
  // Split this across lines, as most translation tools seem to do,
  // (See --width=number here, for instance: http://www.gnu.org/software/gettext/manual/html_node/msggrep-Invocation.html )
  // so that we can see real changes via git diff.
  const guint MAX_WIDTH = 76;
  const guint MAX_WIDTH_FIRST = 69; //MAX_WIDTH - strlen("msgctxt");
  if(result.size() <= MAX_WIDTH_FIRST)
    return result;

  const char* CHAR_SPACE = " ";

  Glib::ustring remaining = result;
  result.clear();
  while(!remaining.empty())
  {
    if(result.empty())
      result = "\"\n\"";
    else
      result += "\"\n\"";

    //result += ("line-max=" + Glib::ustring::format(max));
    if(remaining.size() <= MAX_WIDTH)
    {
      result += remaining;
      remaining.clear();
    }
    else
    {
      //Break after a space, if any:
      Glib::ustring part;
Murray Cumming's avatar
Murray Cumming committed
172
      const auto pos = remaining.find_last_of(CHAR_SPACE, MAX_WIDTH);
173
174
175
176
177
178
179
180
181
      if(pos == Glib::ustring::npos)
        part = remaining.substr(0, MAX_WIDTH);
      else
        part = remaining.substr(0, pos + 1); //Include the space.

      result += part;
      remaining = remaining.substr(part.size());
    }
  }
Murray Cumming's avatar
Murray Cumming committed
182

183
  return result;
184
185
}

186
bool write_pot_file(const std::shared_ptr<Document>& document, const Glib::ustring& pot_file_uri)
187
{
Murray Cumming's avatar
Murray Cumming committed
188
  //A .pot file
189
190
191
  return write_translations_to_po_file(document, pot_file_uri, Glib::ustring() /* no locale */);
}

192
bool write_translations_to_po_file(const std::shared_ptr<Document>& document, const Glib::ustring& po_file_uri, const Glib::ustring& translation_locale, const Glib::ustring& locale_name)
193
194
195
196
197
198
199
200
201
202
203
204
205
{
  std::string filename;

  try
  {
    filename = Glib::filename_from_uri(po_file_uri);
  }
  catch(const Glib::Error& ex)
  {
    std::cerr << G_STRFUNC << "Exception when converting URI to filepath: " << ex.what() << std::endl;
    return false;
  }

206
  //We do not use gettext-po.h and its po_file_write() function for this,
Murray Cumming's avatar
Murray Cumming committed
207
  //because that does not allow us to specify UTF-8, so it drops non-ASCII
208
  //characters such as U with umlaut.
Murray Cumming's avatar
Murray Cumming committed
209
  //It also has no obvious API for setting the header, so we would have to
210
211
  //do that manually anyway.
  Glib::ustring data;
212

213
  for(const auto& the_pair : document->get_translatable_items())
214
  {
215
    auto item = the_pair.first;
216
217
218
219
220
221
    if(!item)
      continue;

    if(item->get_title_original().empty())
      continue;

Murray Cumming's avatar
Murray Cumming committed
222
    const Glib::ustring hint = the_pair.second;
223

224
225
    // Add "context" comments, to uniquely identify similar strings, used in different places,
    // and to provide a hint for translators.
226
    Glib::ustring msg = "msgctxt \"" + get_po_context_for_item(item, hint) + "\"\n";
Murray Cumming's avatar
Murray Cumming committed
227

228
229
230
    //The original and its translation:
    msg += "msgid \"" + item->get_title_original() + "\"\n";
    msg += "msgstr \"" + item->get_title_translation(translation_locale, false) + "\"";
Murray Cumming's avatar
Murray Cumming committed
231

232
    data += msg + "\n\n";
233
234
  }

235
  //The header:
Murray Cumming's avatar
Murray Cumming committed
236
237
238
  const auto revision_date = Glib::DateTime::create_now_local();
  const auto revision_date_str = revision_date.format("%F %R%z");
  const auto header = Glib::ustring::compose(GLOM_PO_HEADER,
239
    document->get_database_title_original(), revision_date_str, locale_name);
Murray Cumming's avatar
Murray Cumming committed
240

241
242
  const Glib::ustring full = header + "\n\n" + data;
  Glib::file_set_contents(filename, full);
243
244
245
246

  return true;
}

247
bool import_translations_from_po_file(const std::shared_ptr<Document>& document, const Glib::ustring& po_file_uri, const Glib::ustring& translation_locale)
248
249
250
251
252
253
254
255
256
257
258
259
260
{
  std::string filename;

  try
  {
    filename = Glib::filename_from_uri(po_file_uri);
  }
  catch(const Glib::Error& ex)
  {
    std::cerr << G_STRFUNC << "Exception when converting URI to filepath: " << ex.what() << std::endl;
    return false;
  }

261
  const auto list_layout_items = document->get_translatable_items();
262
263
264
265
  if(list_layout_items.empty())
    return false;

  if(setjmp(jump) != 0)
Murray Cumming's avatar
Murray Cumming committed
266
    return false;
267
268
269
270
271
272
273
274
275
276
277
278

  #ifdef HAVE_GETTEXTPO_XERROR
  po_xerror_handler error_handler;
  memset(&error_handler, 0, sizeof(error_handler));
  error_handler.xerror = &on_gettextpo_xerror;
  error_handler.xerror2 = &on_gettextpo_xerror2;
  #else
  po_error_handler error_handler;
  memset(&error_handler, 0, sizeof(error_handler));
  error_handler.error = &on_gettextpo_error;
  #endif //HAVE_GETTEXTPO_XERROR

Murray Cumming's avatar
Murray Cumming committed
279
  auto po_file = po_file_read(filename.c_str(), &error_handler);
280
281
282
283
284
285
286
287
  if(!po_file)
  {
    // error message is already given by error_handle.
    return false;
  }

  //Look at each domain (could there be more than one?):
  const char* const* domains = po_file_domains(po_file);
Murray Cumming's avatar
Murray Cumming committed
288
  for (int i = 0; domains[i] != nullptr; ++i)
289
290
  {
    //Look at each message:
Murray Cumming's avatar
Murray Cumming committed
291
    auto iter = po_message_iterator(po_file, domains[i]);
292
293
294
295
296
297
    po_message_t msg;
    while ((msg = po_next_message(iter)))
    {
      //This message:
      //TODO: Just use const char* instead of copying it in to a Glib::ustring,
      //if we have performance problems here:
Murray Cumming's avatar
Murray Cumming committed
298
299
300
      const auto msgid = Glib::convert_const_gchar_ptr_to_ustring( po_message_msgid(msg) );
      const auto msgstr = Glib::convert_const_gchar_ptr_to_ustring( po_message_msgstr(msg) );
      const auto msgcontext = Glib::convert_const_gchar_ptr_to_ustring( po_message_msgctxt(msg) );
301
302

      //Find the matching item in the list:
Murray Cumming's avatar
Murray Cumming committed
303
      for(const auto& the_pair : list_layout_items)
304
      {
305
        const auto& item = the_pair.first;
306
        if(!item)
307
308
          continue;

309
        const auto& hint = the_pair.second;
310

Murray Cumming's avatar
Murray Cumming committed
311
        if( (item->get_title_original() == msgid) &&
312
          (get_po_context_for_item(item, hint) == msgcontext) ) // This is not efficient, but it should be reliable.
313
        {
314
          item->set_title(msgstr, translation_locale);
315
316
317
318
319
320
321
322
323
324
          // Keep examining items, in case there are duplicates. break;
        }
      }
    }

    po_message_iterator_free(iter);
  }

  po_file_free(po_file);

325
326
  document->set_modified();

327
328
329
330
  return true;
}

} //namespace Glom