summaryrefslogtreecommitdiffstats
path: root/gettext-tools/src/write-qt.c
blob: 42b2d847a536f667e0bf7c707fe5c42233955a86 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
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
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
/* Writing Qt .qm files.
   Copyright (C) 2003, 2005-2007, 2009, 2015 Free Software Foundation,
   Inc.
   Written by Bruno Haible <bruno@clisp.org>, 2003.

   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 3 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, see <http://www.gnu.org/licenses/>.  */

#ifdef HAVE_CONFIG_H
# include <config.h>
#endif

/* Specification.  */
#include "write-qt.h"

#include <assert.h>
#include <errno.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "error.h"
#include "xerror.h"
#include "message.h"
#include "po-charset.h"
#include "msgl-iconv.h"
#include "hash-string.h"
#include "unistr.h"
#include "xalloc.h"
#include "obstack.h"
#include "hash.h"
#include "binary-io.h"
#include "fwriteerror.h"
#include "gettext.h"

#define _(str) gettext (str)

/* Qt .qm files are read by the QTranslator::load() function and written
   by the Qt QTranslator::save() function.

   The Qt tool 'msg2qm' uses the latter function and can convert PO files
   to .qm files. But since 'msg2qm' is marked as an "old" tool in Qt 3.0.5's
   i18n.html documentation and therefore likely to disappear, we provide the
   same functionality here.

   The format of .qm files, as reverse engineered from the functions
     QTranslator::save(const QString& filename, SaveMode mode)
     QTranslator::squeeze(SaveMode mode)
     QTranslatorMessage::write(QDataStream& stream, bool strip, Prefix prefix)
     elfHash(const char* name)
   in qt-3.0.5, is as follows:

     It's a binary data format. Elements are u8 (byte), u16, u32. They are
     written in big-endian order.

     The file starts with a magic string of 16 bytes:
       3C B8 64 18 CA EF 9C 95 CD 21 1C BF 60 A1 BD DD

     Then come three sections. Each of the three sections is optional. Each
     has this structure:
       struct {
         u8 section_type; // 0x42 = hashes, 0x69 = messages, 0x2f = contexts
         u32 length; // number of bytes of the data
         u8 data[length];
       };

     In the first section, the hashes section, the data has the following
     structure:
       It's a sorted array of
         struct {
           u32 hashcode; // elfHash of the concatenation of msgid and
                         // disambiguating-comment
           u32 offset; // offset within the data[] of the messages section
         };
       It's sorted in ascending order by hashcode as primary sorting criteria
       and - when the hashcodes are the same - by offset as secondary criteria.

     In the second section, the messages section, the data has the following
     structure:
       It's a sequence of records, each representing a message, in no
       particular order. Each record is a sequence of subsections, each
       introduced by a particular subsection tag. The possible subsection tags
       are (and they usually occur in this order):
         - 03: Translation. Followed by the msgstr in UCS-2 or UTF-16 format:
               struct {
                 u32 length;
                 u16 chars[length/2];
               };
         - 08: Disambiguating-comment. Followed by the NUL-terminated,
               ISO-8859-1 encoded, disambiguating-comment string:
               struct {
                 u32 length;    // number of bytes including the NUL at the end
                 u8 chars[length];
               };
         - 06: SourceText, i.e. msgid. Followed by the NUL-terminated,
               ISO-8859-1 encoded, msgid:
               struct {
                 u32 length;    // number of bytes including the NUL at the end
                 u8 chars[length];
               };
         - 02: SourceText16, i.e. msgid. Encoded as UCS-2, but must actually
               be ISO-8859-1.
               struct {
                 u32 length;
                 u16 chars[length/2];
               };
               This subsection tag is obsoleted by SourceText.
         - 07: Context. Followed by the NUL-terminated, ISO-8859-1 encoded,
               context string (usually a C++ class name or empty):
               struct {
                 u32 length;    // number of bytes including the NUL at the end
                 u8 chars[length];
               };
         - 04: Context16. Encoded as UCS-2, but must actually be ISO-8859-1.
               struct {
                 u32 length;
                 u16 chars[length/2];
               };
               This subsection tag is obsoleted by Context.
         - 05: Hash. Followed by
               struct {
                 u32 hashcode; // elfHash of the concatenation of msgid and
                               // disambiguating-comment
               };
         - 01: End. Designates the end of the record. No further data.
       Usually the following subsections are written, but some of them are
       optional:
         - 03: Translation.
         - 08: Disambiguating-comment (optional).
         - 06: SourceText (optional).
         - 07: Context (optional).
         - 05: Hash.
         - 01: End.
       A subsection can be omitted if the value to be output is the same as
       for the previous record.

     The third section, the contexts section, contains the set of all occurring
     context strings. This section is optional; it is used to speed up the
     search. The data is a hash table with the following structure:
       struct {
         u16 table_size;
         u16 buckets[table_size];
         u8 pool[...];
       };
     pool[...] contains:
       u16 zero;
       for i = 0, ..., table_size:
         if there are context strings with elfHash(context)%table_size == i:
           for all context strings with elfHash(context)%table_size == i:
             len := min(length(context),255); // truncated to length 255
             struct {
               u8 len;
               u8 chars[len];
             };
           struct {
             u8 zero[1]; // signals the end of this bucket
             u8 padding[0 or 1]; // padding for even number of bytes
           };
     buckets[i] is 0 for an empty bucket, or the offset in pool[] where
     the context strings for this bucket start, divided by 2.
     This context section must not be used
       - if the empty context is used, or
       - if a context of length > 255 is used, or
       - if the context pool's size would be > 2^17.

     The elfHash function is the same as our hash_string function, except that
     at the end it maps a hash code of 0x00000000 to 0x00000001.

   When we convert from PO file format, all disambiguating-comments and
   contexts are empty, and therefore the contexts section can be omitted.  */


/* Write a u8 (a single byte) to the output stream.  */
static inline void
write_u8 (FILE *output_file, unsigned char value)
{
  putc (value, output_file);
}

/* Write a u16 (two bytes) to the output stream.  */
static inline void
write_u16 (FILE *output_file, unsigned short value)
{
  unsigned char data[2];

  data[0] = (value >> 8) & 0xff;
  data[1] = value & 0xff;

  fwrite (data, 2, 1, output_file);
}

/* Write a u32 (four bytes) to the output stream.  */
static inline void
write_u32 (FILE *output_file, unsigned int value)
{
  unsigned char data[4];

  data[0] = (value >> 24) & 0xff;
  data[1] = (value >> 16) & 0xff;
  data[2] = (value >> 8) & 0xff;
  data[3] = value & 0xff;

  fwrite (data, 4, 1, output_file);
}


#define obstack_chunk_alloc xmalloc
#define obstack_chunk_free free

/* Add a u8 (a single byte) to an obstack.  */
static void
append_u8 (struct obstack *mempool, unsigned char value)
{
  unsigned char data[1];

  data[0] = value;

  obstack_grow (mempool, data, 1);
}

/* Add a u16 (two bytes) to an obstack.  */
static void
append_u16 (struct obstack *mempool, unsigned short value)
{
  unsigned char data[2];

  data[0] = (value >> 8) & 0xff;
  data[1] = value & 0xff;

  obstack_grow (mempool, data, 2);
}

/* Add a u32 (four bytes) to an obstack.  */
static void
append_u32 (struct obstack *mempool, unsigned int value)
{
  unsigned char data[4];

  data[0] = (value >> 24) & 0xff;
  data[1] = (value >> 16) & 0xff;
  data[2] = (value >> 8) & 0xff;
  data[3] = value & 0xff;

  obstack_grow (mempool, data, 4);
}

/* Add an ISO-8859-1 encoded string to an obstack.  */
static void
append_base_string (struct obstack *mempool, const char *string)
{
  size_t length = strlen (string) + 1;
  append_u32 (mempool, length);
  obstack_grow (mempool, string, length);
}

/* Add an UTF-16 encoded string to an obstack.  */
static void
append_unicode_string (struct obstack *mempool, const unsigned short *string,
                       size_t length)
{
  append_u32 (mempool, length * 2);
  for (; length > 0; string++, length--)
    append_u16 (mempool, *string);
}

/* Retrieve a 4-byte integer from memory.  */
static inline unsigned int
peek_u32 (const unsigned char *p)
{
  return (p[0] << 24) | (p[1] << 16) | (p[2] << 8) | p[3];
}

/* Convert an UTF-8 string to ISO-8859-1, without error checking.  */
static char *
conv_to_iso_8859_1 (const char *string)
{
  size_t length = strlen (string);
  const char *str = string;
  const char *str_limit = string + length;
  /* Conversion to ISO-8859-1 can only reduce the number of bytes.  */
  char *result = XNMALLOC (length + 1, char);
  char *q = result;

  while (str < str_limit)
    {
      ucs4_t uc;
      str += u8_mbtouc (&uc, (const unsigned char *) str, str_limit - str);
      /* It has already been verified that the string fits in ISO-8859-1.  */
      if (!(uc < 0x100))
        abort ();
      /* Store as ISO-8859-1.  */
      *q++ = (unsigned char) uc;
    }
  *q = '\0';
  assert (q - result <= length);

  return result;
}

/* Convert an UTF-8 string to UTF-16, returning its size (number of UTF-16
   codepoints) in *SIZEP.  */
static unsigned short *
conv_to_utf16 (const char *string, size_t *sizep)
{
  size_t length = strlen (string);
  const char *str = string;
  const char *str_limit = string + length;
  /* Conversion to UTF-16 can at most double the number of bytes.  */
  unsigned short *result = XNMALLOC (length, unsigned short);
  unsigned short *q = result;

  while (str < str_limit)
    {
      ucs4_t uc;
      str += u8_mbtouc (&uc, (const unsigned char *) str, str_limit - str);
      if (uc < 0x10000)
        /* UCS-2 character.  */
        *q++ = (unsigned short) uc;
      else
        {
          /* UTF-16 surrogate.  */
          *q++ = 0xd800 + ((uc - 0x10000) >> 10);
          *q++ = 0xdc00 + ((uc - 0x10000) & 0x3ff);
        }
    }
  assert (q - result <= 2 * length);

  *sizep = q - result;
  return result;
}

/* Return the Qt hash code of a string.  */
static unsigned int
string_hashcode (const char *str)
{
  unsigned int h;

  h = hash_string (str);
  if (h == 0)
    h = 1;
  return h;
}

/* Compare two entries of the hashes section.  */
static int
cmp_hashes (const void *va, const void *vb)
{
  const unsigned char *a = (const unsigned char *) va;
  const unsigned char *b = (const unsigned char *) vb;
  unsigned int a_hashcode = peek_u32 (a);
  unsigned int b_hashcode = peek_u32 (b);

  if (a_hashcode != b_hashcode)
    return (a_hashcode >= b_hashcode ? 1 : -1);
  else
    {
      unsigned int a_offset = peek_u32 (a + 4);
      unsigned int b_offset = peek_u32 (b + 4);

      if (a_offset != b_offset)
        return (a_offset >= b_offset ? 1 : -1);
      else
        return 0;
    }
}


/* Write a section to the output stream.  */
static void
write_section (FILE *output_file, unsigned char tag, void *data, size_t size)
{
  /* A section can be omitted if it is empty.  */
  if (size > 0)
    {
      write_u8 (output_file, tag);
      write_u32 (output_file, size);
      fwrite (data, size, 1, output_file);
    }
}


/* Write an entire .qm file.  */
static void
write_qm (FILE *output_file, message_list_ty *mlp)
{
  static unsigned char magic[16] =
    {
      0x3C, 0xB8, 0x64, 0x18, 0xCA, 0xEF, 0x9C, 0x95,
      0xCD, 0x21, 0x1C, 0xBF, 0x60, 0xA1, 0xBD, 0xDD
    };
  struct obstack hashes_pool;
  struct obstack messages_pool;
  size_t j;

  obstack_init (&hashes_pool);
  obstack_init (&messages_pool);

  /* Prepare the hashes section and the messages section.  */
  for (j = 0; j < mlp->nitems; j++)
    {
      message_ty *mp = mlp->item[j];

      /* No need to emit the header entry, it's not needed at runtime.  */
      if (!is_header (mp))
        {
          char *msgctxt_as_iso_8859_1 =
            conv_to_iso_8859_1 (mp->msgctxt != NULL ? mp->msgctxt : "");
          char *msgid_as_iso_8859_1 = conv_to_iso_8859_1 (mp->msgid);
          size_t msgstr_len;
          unsigned short *msgstr_as_utf16 =
            conv_to_utf16 (mp->msgstr, &msgstr_len);
          unsigned int hashcode = string_hashcode (msgid_as_iso_8859_1);
          unsigned int offset = obstack_object_size (&messages_pool);

          /* Add a record to the hashes section.  */
          append_u32 (&hashes_pool, hashcode);
          append_u32 (&hashes_pool, offset);

          /* Add a record to the messages section.  */

          append_u8 (&messages_pool, 0x03);
          append_unicode_string (&messages_pool, msgstr_as_utf16, msgstr_len);

          append_u8 (&messages_pool, 0x08);
          append_base_string (&messages_pool, "");

          append_u8 (&messages_pool, 0x06);
          append_base_string (&messages_pool, msgid_as_iso_8859_1);

          append_u8 (&messages_pool, 0x07);
          append_base_string (&messages_pool, msgctxt_as_iso_8859_1);

          append_u8 (&messages_pool, 0x05);
          append_u32 (&messages_pool, hashcode);

          append_u8 (&messages_pool, 0x01);

          free (msgstr_as_utf16);
          free (msgid_as_iso_8859_1);
          free (msgctxt_as_iso_8859_1);
        }
    }

  /* Sort the hashes section.  */
  {
    size_t nstrings = obstack_object_size (&hashes_pool) / 8;
    if (nstrings > 0)
      qsort (obstack_base (&hashes_pool), nstrings, 8, cmp_hashes);
  }

  /* Write the magic number.  */
  fwrite (magic, sizeof (magic), 1, output_file);

  /* Write the hashes section.  */
  write_section (output_file, 0x42, obstack_base (&hashes_pool),
                 obstack_object_size (&hashes_pool));

  /* Write the messages section.  */
  write_section (output_file, 0x69, obstack_base (&messages_pool),
                 obstack_object_size (&messages_pool));

  /* Decide whether to write a contexts section.  */
  {
    bool can_write_contexts = true;

    for (j = 0; j < mlp->nitems; j++)
      {
        message_ty *mp = mlp->item[j];

        if (!is_header (mp))
          if (mp->msgctxt == NULL || mp->msgctxt[0] == '\0'
              || strlen (mp->msgctxt) > 255)
            {
              can_write_contexts = false;
              break;
            }
      }

    if (can_write_contexts)
      {
        hash_table all_contexts;
        size_t num_contexts;
        unsigned long table_size;

        /* Collect the contexts, removing duplicates.  */
        hash_init (&all_contexts, 10);
        for (j = 0; j < mlp->nitems; j++)
          {
            message_ty *mp = mlp->item[j];

            if (!is_header (mp))
              hash_insert_entry (&all_contexts,
                                 mp->msgctxt, strlen (mp->msgctxt) + 1,
                                 NULL);
          }

        /* Compute the number of different contexts.  */
        num_contexts = all_contexts.size;

        /* Compute a suitable hash table size.  */
        table_size = next_prime (num_contexts * 1.7);
        if (table_size >= 0x10000)
          table_size = 65521;

        /* Put the contexts into a hash table of size table_size.  */
        {
          struct list_cell { const char *context; struct list_cell *next; };
          struct list_cell *list_memory =
            XNMALLOC (table_size, struct list_cell);
          struct list_cell *freelist;
          struct bucket { struct list_cell *head; struct list_cell **tail; };
          struct bucket *buckets = XNMALLOC (table_size, struct bucket);
          size_t i;

          freelist = list_memory;

          for (i = 0; i < table_size; i++)
            {
              buckets[i].head = NULL;
              buckets[i].tail = &buckets[i].head;
            }

          {
            void *iter;
            const void *key;
            size_t keylen;
            void *null;

            iter = NULL;
            while (hash_iterate (&all_contexts, &iter, &key, &keylen, &null)
                   == 0)
              {
                const char *context = (const char *)key;
                i = string_hashcode (context) % table_size;
                freelist->context = context;
                freelist->next = NULL;
                *buckets[i].tail = freelist;
                buckets[i].tail = &freelist->next;
                freelist++;
              }
          }

          /* Determine the total context pool size.  */
          {
            size_t pool_size;

            pool_size = 2;
            for (i = 0; i < table_size; i++)
              if (buckets[i].head != NULL)
                {
                  const struct list_cell *p;

                  for (p = buckets[i].head; p != NULL; p = p->next)
                    pool_size += 1 + strlen (p->context);
                  pool_size++;
                  if ((pool_size % 2) != 0)
                    pool_size++;
                }
            if (pool_size <= 0x20000)
              {
                /* Prepare the contexts section.  */
                struct obstack contexts_pool;
                size_t pool_offset;

                obstack_init (&contexts_pool);

                append_u16 (&contexts_pool, table_size);
                pool_offset = 2;
                for (i = 0; i < table_size; i++)
                  if (buckets[i].head != NULL)
                    {
                      const struct list_cell *p;

                      append_u16 (&contexts_pool, pool_offset / 2);
                      for (p = buckets[i].head; p != NULL; p = p->next)
                        pool_offset += 1 + strlen (p->context);
                      pool_offset++;
                      if ((pool_offset % 2) != 0)
                        pool_offset++;
                    }
                  else
                    append_u16 (&contexts_pool, 0);
                if (!(pool_offset == pool_size))
                  abort ();

                append_u16 (&contexts_pool, 0);
                pool_offset = 2;
                for (i = 0; i < table_size; i++)
                  if (buckets[i].head != NULL)
                    {
                      const struct list_cell *p;

                      for (p = buckets[i].head; p != NULL; p = p->next)
                        {
                          append_u8 (&contexts_pool, strlen (p->context));
                          obstack_grow (&contexts_pool,
                                        p->context, strlen (p->context));
                          pool_offset += 1 + strlen (p->context);
                        }
                      append_u8 (&contexts_pool, 0);
                      pool_offset++;
                      if ((pool_offset % 2) != 0)
                        {
                          append_u8 (&contexts_pool, 0);
                          pool_offset++;
                        }
                    }
                if (!(pool_offset == pool_size))
                  abort ();

                if (!(obstack_object_size (&contexts_pool)
                      == 2 + 2 * table_size + pool_size))
                  abort ();

                /* Write the contexts section.  */
                write_section (output_file, 0x2f, obstack_base (&contexts_pool),
                               obstack_object_size (&contexts_pool));

                obstack_free (&contexts_pool, NULL);
              }
          }

          free (buckets);
          free (list_memory);
        }

        hash_destroy (&all_contexts);
      }
  }

  obstack_free (&messages_pool, NULL);
  obstack_free (&hashes_pool, NULL);
}


int
msgdomain_write_qt (message_list_ty *mlp, const char *canon_encoding,
                    const char *domain_name, const char *file_name)
{
  FILE *output_file;

  /* If no entry for this domain don't even create the file.  */
  if (mlp->nitems != 0)
    {
      /* Determine whether mlp has plural entries.  */
      {
        bool has_plural;
        size_t j;

        has_plural = false;
        for (j = 0; j < mlp->nitems; j++)
          if (mlp->item[j]->msgid_plural != NULL)
            has_plural = true;
        if (has_plural)
          {
            multiline_error (xstrdup (""),
                             xstrdup (_("\
message catalog has plural form translations\n\
but the Qt message catalog format doesn't support plural handling\n")));
            return 1;
          }
      }

      /* Convert the messages to Unicode.  */
      iconv_message_list (mlp, canon_encoding, po_charset_utf8, NULL);

      /* Determine whether mlp has non-ISO-8859-1 msgctxt entries.  */
      {
        size_t j;

        for (j = 0; j < mlp->nitems; j++)
          {
            const char *string = mlp->item[j]->msgctxt;

            if (string != NULL)
              {
                /* An UTF-8 encoded string fits in ISO-8859-1 if and only if
                   all its bytes are < 0xc4.  */
                for (; *string; string++)
                  if ((unsigned char) *string >= 0xc4)
                    {
                      multiline_error (xstrdup (""),
                                       xstrdup (_("\
message catalog has msgctxt strings containing characters outside ISO-8859-1\n\
but the Qt message catalog format supports Unicode only in the translated\n\
strings, not in the context strings\n")));
                      return 1;
                    }
              }
          }
      }

      /* Determine whether mlp has non-ISO-8859-1 msgid entries.  */
      {
        size_t j;

        for (j = 0; j < mlp->nitems; j++)
          {
            const char *string = mlp->item[j]->msgid;

            /* An UTF-8 encoded string fits in ISO-8859-1 if and only if all
               its bytes are < 0xc4.  */
            for (; *string; string++)
              if ((unsigned char) *string >= 0xc4)
                {
                  multiline_error (xstrdup (""),
                                   xstrdup (_("\
message catalog has msgid strings containing characters outside ISO-8859-1\n\
but the Qt message catalog format supports Unicode only in the translated\n\
strings, not in the untranslated strings\n")));
                  return 1;
                }
          }
      }

      if (strcmp (domain_name, "-") == 0)
        {
          output_file = stdout;
          SET_BINARY (fileno (output_file));
        }
      else
        {
          output_file = fopen (file_name, "wb");
          if (output_file == NULL)
            {
              error (0, errno, _("error while opening \"%s\" for writing"),
                     file_name);
              return 1;
            }
        }

      if (output_file != NULL)
        {
          write_qm (output_file, mlp);

          /* Make sure nothing went wrong.  */
          if (fwriteerror (output_file))
            error (EXIT_FAILURE, errno, _("error while writing \"%s\" file"),
                   file_name);
        }
    }

  return 0;
}